Compare commits
18 Commits
1.0.0
...
7f744d04b0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f744d04b0 | |||
| 6c98d03ae7 | |||
| 17ff507c89 | |||
| 539f14e8ec | |||
| 1673e9554a | |||
| b83836f914 | |||
| 745856f0f4 | |||
| b4d2b1305e | |||
| 0d263be2ca | |||
| 63a1c21861 | |||
| 0a73e46f90 | |||
| 6f1158edeb | |||
| 93b682cfa5 | |||
| f378d0e81f | |||
| 8c1bb695b8 | |||
| f58951fd92 | |||
| 70786da8c6 | |||
| e24af47697 |
@@ -8,14 +8,14 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@main
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- run: git fetch --force --tags
|
- run: git fetch --force --tags
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@main
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: 'go.mod'
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
- uses: goreleaser/goreleaser-action@master
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: 'latest'
|
version: 'latest'
|
||||||
|
|||||||
@@ -6,14 +6,10 @@ jobs:
|
|||||||
check-and-test:
|
check-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@main
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@main
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: 'go.mod'
|
||||||
- run: go mod tidy
|
- run: go mod tidy
|
||||||
- uses: golangci/golangci-lint-action@v3
|
|
||||||
with:
|
|
||||||
args: -D errcheck
|
|
||||||
version: latest
|
|
||||||
- run: go build ./...
|
- run: go build ./...
|
||||||
- run: go test -race -v -shuffle=on ./...
|
- run: go test -race -v -shuffle=on ./...
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
dist/
|
dist/
|
||||||
tmp/
|
tmp/
|
||||||
__*.exe
|
__*.exe
|
||||||
|
.smashed.txt
|
||||||
|
.smashignore
|
||||||
@@ -16,7 +16,7 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- format: tar.gz
|
- formats: tar.gz
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{ .ProjectName }}_
|
{{ .ProjectName }}_
|
||||||
{{- title .Os }}_
|
{{- title .Os }}_
|
||||||
@@ -26,7 +26,7 @@ archives:
|
|||||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats: zip
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
|
|||||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -17,6 +17,7 @@
|
|||||||
"10G",
|
"10G",
|
||||||
"--disk-path",
|
"--disk-path",
|
||||||
"tmp/disk",
|
"tmp/disk",
|
||||||
|
"--verbose",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"10G",
|
"10G",
|
||||||
"--disk-path",
|
"--disk-path",
|
||||||
"tmp/disk",
|
"tmp/disk",
|
||||||
|
"--verbose",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"--memory",
|
"--memory",
|
||||||
"1G",
|
"1G",
|
||||||
|
"--verbose",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
59
cmd/root.go
59
cmd/root.go
@@ -1,18 +1,29 @@
|
|||||||
|
// cmd/root.go
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"s1d3sw1ped/SteamCache2/steamcache"
|
"s1d3sw1ped/SteamCache2/steamcache"
|
||||||
|
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||||
|
"s1d3sw1ped/SteamCache2/version"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
threads int
|
||||||
|
|
||||||
memory string
|
memory string
|
||||||
memorymultiplier int
|
memorymultiplier int
|
||||||
disk string
|
disk string
|
||||||
diskmultiplier int
|
diskmultiplier int
|
||||||
diskpath string
|
diskpath string
|
||||||
|
upstream string
|
||||||
|
|
||||||
|
logLevel string
|
||||||
|
logFormat string
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -24,15 +35,54 @@ var rootCmd = &cobra.Command{
|
|||||||
By caching game files, SteamCache2 ensures that subsequent downloads of the same files are served from the local cache,
|
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.`,
|
significantly improving download times and reducing the load on the internet connection.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// Configure logging
|
||||||
|
switch logLevel {
|
||||||
|
case "debug":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
case "error":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
case "info":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
default:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel) // Default to info level if not specified
|
||||||
|
}
|
||||||
|
var writer zerolog.ConsoleWriter
|
||||||
|
if logFormat == "json" {
|
||||||
|
writer = zerolog.ConsoleWriter{Out: os.Stderr, NoColor: true}
|
||||||
|
} else {
|
||||||
|
writer = zerolog.ConsoleWriter{Out: os.Stderr}
|
||||||
|
}
|
||||||
|
logger.Logger = zerolog.New(writer).With().Timestamp().Logger()
|
||||||
|
|
||||||
|
logger.Logger.Info().
|
||||||
|
Msg("SteamCache2 " + version.Version + " starting...")
|
||||||
|
|
||||||
|
address := ":80"
|
||||||
|
|
||||||
|
if runtime.GOMAXPROCS(-1) != threads {
|
||||||
|
runtime.GOMAXPROCS(threads)
|
||||||
|
logger.Logger.Info().
|
||||||
|
Int("threads", threads).
|
||||||
|
Msg("Maximum number of threads set")
|
||||||
|
}
|
||||||
|
|
||||||
sc := steamcache.New(
|
sc := steamcache.New(
|
||||||
":80",
|
address,
|
||||||
memory,
|
memory,
|
||||||
memorymultiplier,
|
memorymultiplier,
|
||||||
disk,
|
disk,
|
||||||
diskmultiplier,
|
diskmultiplier,
|
||||||
diskpath,
|
diskpath,
|
||||||
|
upstream,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.Logger.Info().
|
||||||
|
Msg("SteamCache2 " + version.Version + " started on " + address)
|
||||||
|
|
||||||
sc.Run()
|
sc.Run()
|
||||||
|
|
||||||
|
logger.Logger.Info().Msg("SteamCache2 stopped")
|
||||||
|
os.Exit(0)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,9 +96,16 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
rootCmd.Flags().IntVarP(&threads, "threads", "t", runtime.GOMAXPROCS(-1), "Number of worker threads to use for processing requests")
|
||||||
|
|
||||||
rootCmd.Flags().StringVarP(&memory, "memory", "m", "0", "The size of the memory cache")
|
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().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().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().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(&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().StringVarP(&logLevel, "log-level", "l", "info", "Logging level: debug, info, error")
|
||||||
|
rootCmd.Flags().StringVarP(&logFormat, "log-format", "f", "console", "Logging format: json, console")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// cmd/version.go
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -4,15 +4,22 @@ go 1.23.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
34
go.sum
34
go.sum
@@ -1,16 +1,40 @@
|
|||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
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/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||||
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
@@ -19,11 +43,15 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
|||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package avgcachestate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs/cachestate"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AvgCacheState is a cache state that averages the last N cache states.
|
|
||||||
type AvgCacheState struct {
|
|
||||||
size int
|
|
||||||
avgs []cachestate.CacheState
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new average cache state with the given size.
|
|
||||||
func New(size int) *AvgCacheState {
|
|
||||||
a := &AvgCacheState{
|
|
||||||
size: size,
|
|
||||||
avgs: make([]cachestate.CacheState, size),
|
|
||||||
mu: sync.Mutex{},
|
|
||||||
}
|
|
||||||
|
|
||||||
a.Clear()
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear resets the average cache state to zero.
|
|
||||||
func (a *AvgCacheState) Clear() {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
|
|
||||||
for i := 0; i < len(a.avgs); i++ {
|
|
||||||
a.avgs[i] = cachestate.CacheStateMiss
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a cache state to the average cache state.
|
|
||||||
func (a *AvgCacheState) Add(cs cachestate.CacheState) {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
|
|
||||||
a.avgs = append(a.avgs, cs)
|
|
||||||
if len(a.avgs) > a.size {
|
|
||||||
a.avgs = a.avgs[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avg returns the average cache state.
|
|
||||||
func (a *AvgCacheState) Avg() float64 {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
|
|
||||||
var hits int
|
|
||||||
|
|
||||||
for _, cs := range a.avgs {
|
|
||||||
if cs == cachestate.CacheStateHit {
|
|
||||||
hits++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return float64(hits) / float64(len(a.avgs))
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package steamcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs/cachestate"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/exp/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
deletions := 0
|
|
||||||
targetreclaim := int64(size)
|
|
||||||
var reclaimed int64
|
|
||||||
|
|
||||||
stats := vfss.StatAll()
|
|
||||||
for {
|
|
||||||
reclaimed += random(vfss, stats)
|
|
||||||
deletions++
|
|
||||||
if reclaimed >= targetreclaim {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uint(reclaimed), uint(deletions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cachehandler(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
|
|
||||||
return time.Since(fi.AccessTime()) < time.Minute*10 // Put files in the cache if they've been accessed twice in the last 10 minutes
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
|
// steamcache/logger/logger.go
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger()
|
var Logger zerolog.Logger
|
||||||
|
|||||||
@@ -1,30 +1,59 @@
|
|||||||
|
// steamcache/steamcache.go
|
||||||
package steamcache
|
package steamcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"s1d3sw1ped/SteamCache2/steamcache/avgcachestate"
|
|
||||||
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||||
"s1d3sw1ped/SteamCache2/version"
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
"s1d3sw1ped/SteamCache2/vfs"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/cache"
|
"s1d3sw1ped/SteamCache2/vfs/cache"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/cachestate"
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs/disk"
|
"s1d3sw1ped/SteamCache2/vfs/disk"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/gc"
|
"s1d3sw1ped/SteamCache2/vfs/gc"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/memory"
|
"s1d3sw1ped/SteamCache2/vfs/memory"
|
||||||
syncfs "s1d3sw1ped/SteamCache2/vfs/sync"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
requestsTotal = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "http_requests_total",
|
||||||
|
Help: "Total number of HTTP requests",
|
||||||
|
},
|
||||||
|
[]string{"method", "status"},
|
||||||
|
)
|
||||||
|
|
||||||
|
cacheStatusTotal = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "cache_status_total",
|
||||||
|
Help: "Total cache status counts",
|
||||||
|
},
|
||||||
|
[]string{"status"},
|
||||||
|
)
|
||||||
|
|
||||||
|
responseTime = promauto.NewHistogram(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "response_time_seconds",
|
||||||
|
Help: "Response time in seconds",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
type SteamCache struct {
|
type SteamCache struct {
|
||||||
address string
|
address string
|
||||||
|
upstream string
|
||||||
|
|
||||||
vfs vfs.VFS
|
vfs vfs.VFS
|
||||||
|
|
||||||
memory *memory.MemoryFS
|
memory *memory.MemoryFS
|
||||||
@@ -33,13 +62,13 @@ type SteamCache struct {
|
|||||||
memorygc *gc.GCFS
|
memorygc *gc.GCFS
|
||||||
diskgc *gc.GCFS
|
diskgc *gc.GCFS
|
||||||
|
|
||||||
hits *avgcachestate.AvgCacheState
|
server *http.Server
|
||||||
|
client *http.Client
|
||||||
dirty bool
|
cancel context.CancelFunc
|
||||||
mu sync.Mutex
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(address string, memorySize string, memoryMultiplier int, diskSize string, diskMultiplier int, diskPath string) *SteamCache {
|
func New(address string, memorySize string, memoryMultiplier int, diskSize string, diskMultiplier int, diskPath, upstream string) *SteamCache {
|
||||||
memorysize, err := units.FromHumanSize(memorySize)
|
memorysize, err := units.FromHumanSize(memorySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -51,21 +80,21 @@ func New(address string, memorySize string, memoryMultiplier int, diskSize strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
c := cache.New(
|
c := cache.New(
|
||||||
cachehandler,
|
gc.PromotionDecider,
|
||||||
)
|
)
|
||||||
|
|
||||||
var m *memory.MemoryFS
|
var m *memory.MemoryFS
|
||||||
var mgc *gc.GCFS
|
var mgc *gc.GCFS
|
||||||
if memorysize > 0 {
|
if memorysize > 0 {
|
||||||
m = memory.New(memorysize)
|
m = memory.New(memorysize)
|
||||||
mgc = gc.New(m, memoryMultiplier, randomgc)
|
mgc = gc.New(m, memoryMultiplier, gc.LRUGC)
|
||||||
}
|
}
|
||||||
|
|
||||||
var d *disk.DiskFS
|
var d *disk.DiskFS
|
||||||
var dgc *gc.GCFS
|
var dgc *gc.GCFS
|
||||||
if disksize > 0 {
|
if disksize > 0 {
|
||||||
d = disk.New(diskPath, disksize)
|
d = disk.New(diskPath, disksize)
|
||||||
dgc = gc.New(d, diskMultiplier, randomgc)
|
dgc = gc.New(d, diskMultiplier, gc.LRUGC)
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure the cache to match the specified mode (memory only, disk only, or memory and disk) based on the provided sizes
|
// configure the cache to match the specified mode (memory only, disk only, or memory and disk) based on the provided sizes
|
||||||
@@ -73,40 +102,59 @@ func New(address string, memorySize string, memoryMultiplier int, diskSize strin
|
|||||||
//memory only mode - no disk
|
//memory only mode - no disk
|
||||||
|
|
||||||
c.SetSlow(mgc)
|
c.SetSlow(mgc)
|
||||||
logger.Logger.Info().Bool("memory", true).Bool("disk", false).Msg("configuration")
|
|
||||||
} else if disksize != 0 && memorysize == 0 {
|
} else if disksize != 0 && memorysize == 0 {
|
||||||
// disk only mode
|
// disk only mode
|
||||||
|
|
||||||
c.SetSlow(dgc)
|
c.SetSlow(dgc)
|
||||||
logger.Logger.Info().Bool("memory", false).Bool("disk", true).Msg("configuration")
|
|
||||||
} else if disksize != 0 && memorysize != 0 {
|
} else if disksize != 0 && memorysize != 0 {
|
||||||
// memory and disk mode
|
// memory and disk mode
|
||||||
|
|
||||||
c.SetFast(mgc)
|
c.SetFast(mgc)
|
||||||
c.SetSlow(dgc)
|
c.SetSlow(dgc)
|
||||||
logger.Logger.Info().Bool("memory", true).Bool("disk", true).Msg("configuration")
|
|
||||||
} else {
|
} else {
|
||||||
// no memory or disk isn't a valid configuration
|
// no memory or disk isn't a valid configuration
|
||||||
logger.Logger.Error().Bool("memory", false).Bool("disk", false).Msg("configuration invalid :( exiting")
|
logger.Logger.Error().Bool("memory", false).Bool("disk", false).Msg("configuration invalid :( exiting")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
sc := &SteamCache{
|
transport := &http.Transport{
|
||||||
address: address,
|
MaxIdleConns: 100,
|
||||||
vfs: syncfs.New(c),
|
MaxIdleConnsPerHost: 10,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := &SteamCache{
|
||||||
|
upstream: upstream,
|
||||||
|
address: address,
|
||||||
|
vfs: c,
|
||||||
memory: m,
|
memory: m,
|
||||||
disk: d,
|
disk: d,
|
||||||
|
|
||||||
memorygc: mgc,
|
memorygc: mgc,
|
||||||
diskgc: dgc,
|
diskgc: dgc,
|
||||||
|
client: client,
|
||||||
hits: avgcachestate.New(100),
|
server: &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if d != nil {
|
if d != nil {
|
||||||
if d.Size() > d.Capacity() {
|
if d.Size() > d.Capacity() {
|
||||||
randomgc(d, uint(d.Size()-d.Capacity()))
|
gc.LRUGC(d, uint(d.Size()-d.Capacity()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,84 +162,50 @@ func New(address string, memorySize string, memoryMultiplier int, diskSize strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *SteamCache) Run() {
|
func (sc *SteamCache) Run() {
|
||||||
logger.Logger.Info().Str("address", sc.address).Str("version", version.Version).Msg("listening")
|
if sc.upstream != "" {
|
||||||
|
resp, err := sc.client.Get(sc.upstream)
|
||||||
|
if err != nil || resp.StatusCode != http.StatusOK {
|
||||||
|
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed to connect to upstream server")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
sc.mu.Lock()
|
sc.server.Handler = sc
|
||||||
sc.dirty = true
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
sc.mu.Unlock()
|
sc.cancel = cancel
|
||||||
|
|
||||||
sc.LogStats()
|
sc.wg.Add(1)
|
||||||
t := time.NewTicker(1 * time.Second)
|
|
||||||
go func() {
|
go func() {
|
||||||
for range t.C {
|
defer sc.wg.Done()
|
||||||
sc.LogStats()
|
err := sc.server.ListenAndServe()
|
||||||
}
|
if err != nil && err != http.ErrServerClosed {
|
||||||
}()
|
|
||||||
|
|
||||||
err := http.ListenAndServe(sc.address, sc)
|
|
||||||
if err != nil {
|
|
||||||
if err == http.ErrServerClosed {
|
|
||||||
logger.Logger.Info().Msg("shutdown")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Logger.Error().Err(err).Msg("Failed to start SteamCache2")
|
logger.Logger.Error().Err(err).Msg("Failed to start SteamCache2")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
sc.server.Shutdown(ctx)
|
||||||
|
sc.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *SteamCache) LogStats() {
|
func (sc *SteamCache) Shutdown() {
|
||||||
sc.mu.Lock()
|
if sc.cancel != nil {
|
||||||
defer sc.mu.Unlock()
|
sc.cancel()
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Logger.Info().
|
|
||||||
Str("hitrate", fmt.Sprintf("%.2f%%", sc.hits.Avg()*100)).
|
|
||||||
Msg("cache")
|
|
||||||
|
|
||||||
sc.dirty = false
|
|
||||||
}
|
}
|
||||||
|
sc.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/metrics" {
|
||||||
|
promhttp.Handler().ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "405").Inc()
|
||||||
|
logger.Logger.Warn().Str("method", r.Method).Msg("Only GET method is supported")
|
||||||
http.Error(w, "Only GET method is supported", http.StatusMethodNotAllowed)
|
http.Error(w, "Only GET method is supported", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -203,110 +217,154 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Header.Get("User-Agent") != "Valve/Steam HTTP Client 1.0" {
|
if strings.HasPrefix(r.URL.String(), "/depot/") {
|
||||||
http.Error(w, "Only Valve/Steam HTTP Client 1.0 is supported", http.StatusForbidden)
|
// trim the query parameters from the URL path
|
||||||
|
// this is necessary because the cache key should not include query parameters
|
||||||
|
r.URL.Path = strings.Split(r.URL.Path, "?")[0]
|
||||||
|
|
||||||
|
tstart := time.Now()
|
||||||
|
defer func() { responseTime.Observe(time.Since(tstart).Seconds()) }()
|
||||||
|
|
||||||
|
cacheKey := strings.ReplaceAll(r.URL.String()[1:], "\\", "/") // replace all backslashes with forward slashes shouldn't be necessary but just in case
|
||||||
|
|
||||||
|
if cacheKey == "" {
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "400").Inc()
|
||||||
|
logger.Logger.Warn().Str("url", r.URL.String()).Msg("Invalid URL")
|
||||||
|
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||||
return
|
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
|
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 := r.URL.String()
|
reader, err := sc.vfs.Open(cacheKey)
|
||||||
|
|
||||||
// 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 {
|
if err == nil {
|
||||||
sc.hits.Add(cachestate.CacheStateHit)
|
defer reader.Close()
|
||||||
w.Header().Add("X-LanCache-Status", "HIT")
|
w.Header().Add("X-LanCache-Status", "HIT")
|
||||||
w.Write(data)
|
|
||||||
|
io.Copy(w, reader)
|
||||||
|
|
||||||
|
logger.Logger.Info().
|
||||||
|
Str("key", cacheKey).
|
||||||
|
Str("host", r.Host).
|
||||||
|
Str("status", "HIT").
|
||||||
|
Dur("duration", time.Since(tstart)).
|
||||||
|
Msg("request")
|
||||||
|
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "200").Inc()
|
||||||
|
cacheStatusTotal.WithLabelValues("HIT").Inc()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
htt := "http://"
|
var req *http.Request
|
||||||
if r.Header.Get("X-Sls-Https") == "enable" {
|
if sc.upstream != "" { // if an upstream server is configured, proxy the request to the upstream server
|
||||||
htt = "https://"
|
ur, err := url.JoinPath(sc.upstream, r.URL.String())
|
||||||
}
|
|
||||||
|
|
||||||
base := htt + r.Host
|
|
||||||
|
|
||||||
hosturl, err := url.JoinPath(base, cacheKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "500").Inc()
|
||||||
|
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed to join URL path")
|
||||||
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(hosturl)
|
req, err = http.NewRequest(http.MethodGet, ur, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "500").Inc()
|
||||||
|
logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed to create request")
|
||||||
|
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Host = r.Host
|
||||||
|
} 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 {
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "500").Inc()
|
||||||
|
logger.Logger.Error().Err(err).Str("host", host).Msg("Failed to join URL path")
|
||||||
|
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodGet, ur, nil)
|
||||||
|
if err != nil {
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "500").Inc()
|
||||||
|
logger.Logger.Error().Err(err).Str("host", host).Msg("Failed to create request")
|
||||||
|
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy headers from the original request to the new request
|
||||||
|
for key, values := range r.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry logic
|
||||||
|
backoffSchedule := []time.Duration{1 * time.Second, 3 * time.Second, 10 * time.Second}
|
||||||
|
var resp *http.Response
|
||||||
|
for i, backoff := range backoffSchedule {
|
||||||
|
resp, err = sc.client.Do(req)
|
||||||
|
if err == nil && resp.StatusCode == http.StatusOK {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i < len(backoffSchedule)-1 {
|
||||||
|
time.Sleep(backoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil || resp.StatusCode != http.StatusOK {
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "500 upstream host "+r.Host).Inc()
|
||||||
|
logger.Logger.Error().Err(err).Str("url", req.URL.String()).Msg("Failed to fetch the requested URL")
|
||||||
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
size := resp.ContentLength
|
||||||
http.Error(w, "Failed to fetch the requested URL", resp.StatusCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
writer, err := sc.vfs.Create(cacheKey, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to read response body", http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
sc.vfs.Set(cacheKey, body)
|
|
||||||
sc.hits.Add(cachestate.CacheStateMiss)
|
|
||||||
w.Header().Add("X-LanCache-Status", "MISS")
|
w.Header().Add("X-LanCache-Status", "MISS")
|
||||||
w.Write(body)
|
|
||||||
}
|
io.Copy(io.MultiWriter(w, writer), resp.Body)
|
||||||
|
|
||||||
func forward(w http.ResponseWriter, r *http.Request) {
|
logger.Logger.Info().
|
||||||
htt := "http://"
|
Str("key", cacheKey).
|
||||||
if r.Header.Get("X-Sls-Https") == "enable" {
|
Str("host", r.Host).
|
||||||
htt = "https://"
|
Str("status", "MISS").
|
||||||
}
|
Dur("duration", time.Since(tstart)).
|
||||||
|
Msg("request")
|
||||||
base := htt + r.Host
|
|
||||||
|
requestsTotal.WithLabelValues(r.Method, "200").Inc()
|
||||||
cacheKey := r.URL.String()
|
cacheStatusTotal.WithLabelValues("MISS").Inc()
|
||||||
|
|
||||||
hosturl, err := url.JoinPath(base, cacheKey)
|
return
|
||||||
if err != nil {
|
}
|
||||||
http.Error(w, "Failed to join URL path", http.StatusInternalServerError)
|
|
||||||
return
|
if r.URL.Path == "/favicon.ico" {
|
||||||
}
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
resp, err := http.Get(hosturl)
|
}
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError)
|
if r.URL.Path == "/robots.txt" {
|
||||||
return
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
}
|
w.WriteHeader(http.StatusOK)
|
||||||
defer resp.Body.Close()
|
w.Write([]byte("User-agent: *\nDisallow: /\n"))
|
||||||
|
return
|
||||||
if resp.StatusCode != http.StatusOK {
|
}
|
||||||
http.Error(w, "Failed to fetch the requested URL", resp.StatusCode)
|
|
||||||
return
|
requestsTotal.WithLabelValues(r.Method, "404").Inc()
|
||||||
}
|
logger.Logger.Warn().Str("url", r.URL.String()).Msg("Not found")
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to read response body", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(body)
|
|
||||||
}
|
}
|
||||||
|
|||||||
110
steamcache/steamcache_test.go
Normal file
110
steamcache/steamcache_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// steamcache/steamcache_test.go
|
||||||
|
package steamcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCaching(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
|
||||||
|
os.WriteFile(filepath.Join(td, "key2"), []byte("value2"), 0644)
|
||||||
|
|
||||||
|
sc := New("localhost:8080", "1G", 10, "1G", 100, td, "")
|
||||||
|
|
||||||
|
w, err := sc.vfs.Create("key", 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write([]byte("value"))
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
w, err = sc.vfs.Create("key1", 6)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write([]byte("value1"))
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := sc.vfs.Open("key")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
d, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
if string(d) != "value" {
|
||||||
|
t.Errorf("Get failed: got %s, want %s", d, "value")
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err = sc.vfs.Open("key1")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
d, _ = io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
if string(d) != "value1" {
|
||||||
|
t.Errorf("Get failed: got %s, want %s", d, "value1")
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err = sc.vfs.Open("key2")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
d, _ = io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
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.Open("key2"); err == nil {
|
||||||
|
t.Errorf("Open failed: got nil, want error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheMissAndHit(t *testing.T) {
|
||||||
|
sc := New("localhost:8080", "0", 0, "1G", 100, t.TempDir(), "")
|
||||||
|
|
||||||
|
key := "testkey"
|
||||||
|
value := []byte("testvalue")
|
||||||
|
|
||||||
|
// Simulate miss: but since no upstream, skip full ServeHTTP, test VFS
|
||||||
|
w, err := sc.vfs.Create(key, int64(len(value)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
rc, err := sc.vfs.Open(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
|
if string(got) != string(value) {
|
||||||
|
t.Errorf("expected %s, got %s", value, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
// version/version.go
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var Version string
|
var Version string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if Version == "" {
|
||||||
|
Version = "0.0.0-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
104
vfs/cache/cache.go
vendored
104
vfs/cache/cache.go
vendored
@@ -1,10 +1,13 @@
|
|||||||
|
// vfs/cache/cache.go
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
"s1d3sw1ped/SteamCache2/vfs"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/cachestate"
|
"s1d3sw1ped/SteamCache2/vfs/cachestate"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure CacheFS implements VFS.
|
// Ensure CacheFS implements VFS.
|
||||||
@@ -16,6 +19,8 @@ type CacheFS struct {
|
|||||||
slow vfs.VFS
|
slow vfs.VFS
|
||||||
|
|
||||||
cacheHandler CacheHandler
|
cacheHandler CacheHandler
|
||||||
|
|
||||||
|
keyLocks sync.Map // map[string]*sync.RWMutex for per-key locks
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheHandler func(*vfs.FileInfo, cachestate.CacheState) bool
|
type CacheHandler func(*vfs.FileInfo, cachestate.CacheState) bool
|
||||||
@@ -24,6 +29,7 @@ type CacheHandler func(*vfs.FileInfo, cachestate.CacheState) bool
|
|||||||
func New(cacheHandler CacheHandler) *CacheFS {
|
func New(cacheHandler CacheHandler) *CacheFS {
|
||||||
return &CacheFS{
|
return &CacheFS{
|
||||||
cacheHandler: cacheHandler,
|
cacheHandler: cacheHandler,
|
||||||
|
keyLocks: sync.Map{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +45,12 @@ func (c *CacheFS) SetFast(vfs vfs.VFS) {
|
|||||||
c.fast = vfs
|
c.fast = vfs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getKeyLock returns a RWMutex for the given key, creating it if necessary.
|
||||||
|
func (c *CacheFS) getKeyLock(key string) *sync.RWMutex {
|
||||||
|
mu, _ := c.keyLocks.LoadOrStore(key, &sync.RWMutex{})
|
||||||
|
return mu.(*sync.RWMutex)
|
||||||
|
}
|
||||||
|
|
||||||
// cacheState returns the state of the file at key.
|
// cacheState returns the state of the file at key.
|
||||||
func (c *CacheFS) cacheState(key string) cachestate.CacheState {
|
func (c *CacheFS) cacheState(key string) cachestate.CacheState {
|
||||||
if c.fast != nil {
|
if c.fast != nil {
|
||||||
@@ -63,65 +75,74 @@ func (c *CacheFS) Size() int64 {
|
|||||||
return c.slow.Size()
|
return c.slow.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets the file at key to src. If the file is already in the cache, it is replaced.
|
|
||||||
func (c *CacheFS) Set(key string, src []byte) error {
|
|
||||||
state := c.cacheState(key)
|
|
||||||
|
|
||||||
switch state {
|
|
||||||
case cachestate.CacheStateHit:
|
|
||||||
if c.fast != nil {
|
|
||||||
c.fast.Delete(key)
|
|
||||||
}
|
|
||||||
return c.slow.Set(key, src)
|
|
||||||
case cachestate.CacheStateMiss, cachestate.CacheStateNotFound:
|
|
||||||
return c.slow.Set(key, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(vfserror.ErrUnreachable)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the file at key from the cache.
|
// Delete deletes the file at key from the cache.
|
||||||
func (c *CacheFS) Delete(key string) error {
|
func (c *CacheFS) Delete(key string) error {
|
||||||
|
mu := c.getKeyLock(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
if c.fast != nil {
|
if c.fast != nil {
|
||||||
c.fast.Delete(key)
|
c.fast.Delete(key)
|
||||||
}
|
}
|
||||||
return c.slow.Delete(key)
|
return c.slow.Delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the file at key. If the file is not in the cache, it is fetched from the storage.
|
// Open returns the file at key. If the file is not in the cache, it is fetched from the storage.
|
||||||
func (c *CacheFS) Get(key string) ([]byte, error) {
|
func (c *CacheFS) Open(key string) (io.ReadCloser, error) {
|
||||||
src, _, err := c.GetS(key)
|
mu := c.getKeyLock(key)
|
||||||
return src, err
|
mu.RLock()
|
||||||
}
|
defer mu.RUnlock()
|
||||||
|
|
||||||
// GetS returns the file at key. If the file is not in the cache, it is fetched from the storage. It also returns the cache state.
|
|
||||||
func (c *CacheFS) GetS(key string) ([]byte, cachestate.CacheState, error) {
|
|
||||||
state := c.cacheState(key)
|
state := c.cacheState(key)
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
case cachestate.CacheStateHit:
|
case cachestate.CacheStateHit:
|
||||||
// if c.fast == nil then cacheState cannot be CacheStateHit so we can safely ignore the check
|
// if c.fast == nil then cacheState cannot be CacheStateHit so we can safely ignore the check
|
||||||
src, err := c.fast.Get(key)
|
return c.fast.Open(key)
|
||||||
return src, state, err
|
|
||||||
case cachestate.CacheStateMiss:
|
case cachestate.CacheStateMiss:
|
||||||
src, err := c.slow.Get(key)
|
slowReader, err := c.slow.Open(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, state, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sstat, _ := c.slow.Stat(key)
|
sstat, _ := c.slow.Stat(key)
|
||||||
if sstat != nil && c.fast != nil { // file found in slow storage and fast storage is available
|
if sstat != nil && c.fast != nil { // file found in slow storage and fast storage is available
|
||||||
// We are accessing the file from the slow storage, and the file has been accessed less then a minute ago so it popular, so we should update the fast storage with the latest file.
|
// We are accessing the file from the slow storage, and the file has been accessed less then a minute ago so it popular, so we should update the fast storage with the latest file.
|
||||||
if c.cacheHandler != nil && c.cacheHandler(sstat, state) {
|
if c.cacheHandler != nil && c.cacheHandler(sstat, state) {
|
||||||
if err := c.fast.Set(key, src); err != nil {
|
fastWriter, err := c.fast.Create(key, sstat.Size())
|
||||||
return nil, state, err
|
if err == nil {
|
||||||
|
return &teeReadCloser{
|
||||||
|
Reader: io.TeeReader(slowReader, fastWriter),
|
||||||
|
closers: []io.Closer{slowReader, fastWriter},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return src, state, nil
|
return slowReader, nil
|
||||||
case cachestate.CacheStateNotFound:
|
case cachestate.CacheStateNotFound:
|
||||||
return nil, state, vfserror.ErrNotFound
|
return nil, vfserror.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(vfserror.ErrUnreachable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new file at key. If the file is already in the cache, it is replaced.
|
||||||
|
func (c *CacheFS) Create(key string, size int64) (io.WriteCloser, error) {
|
||||||
|
mu := c.getKeyLock(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
state := c.cacheState(key)
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case cachestate.CacheStateHit:
|
||||||
|
if c.fast != nil {
|
||||||
|
c.fast.Delete(key)
|
||||||
|
}
|
||||||
|
return c.slow.Create(key, size)
|
||||||
|
case cachestate.CacheStateMiss, cachestate.CacheStateNotFound:
|
||||||
|
return c.slow.Create(key, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(vfserror.ErrUnreachable)
|
panic(vfserror.ErrUnreachable)
|
||||||
@@ -130,6 +151,10 @@ func (c *CacheFS) GetS(key string) ([]byte, cachestate.CacheState, error) {
|
|||||||
// Stat returns information about the file at key.
|
// Stat returns information about the file at key.
|
||||||
// Warning: This will return information about the file in the fastest storage its in.
|
// Warning: This will return information about the file in the fastest storage its in.
|
||||||
func (c *CacheFS) Stat(key string) (*vfs.FileInfo, error) {
|
func (c *CacheFS) Stat(key string) (*vfs.FileInfo, error) {
|
||||||
|
mu := c.getKeyLock(key)
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
state := c.cacheState(key)
|
state := c.cacheState(key)
|
||||||
|
|
||||||
switch state {
|
switch state {
|
||||||
@@ -150,3 +175,18 @@ func (c *CacheFS) Stat(key string) (*vfs.FileInfo, error) {
|
|||||||
func (c *CacheFS) StatAll() []*vfs.FileInfo {
|
func (c *CacheFS) StatAll() []*vfs.FileInfo {
|
||||||
return c.slow.StatAll()
|
return c.slow.StatAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type teeReadCloser struct {
|
||||||
|
io.Reader
|
||||||
|
closers []io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *teeReadCloser) Close() error {
|
||||||
|
var err error
|
||||||
|
for _, c := range t.closers {
|
||||||
|
if e := c.Close(); e != nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
115
vfs/cache/cache_test.go
vendored
115
vfs/cache/cache_test.go
vendored
@@ -1,7 +1,9 @@
|
|||||||
|
// vfs/cache/cache_test.go
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
"s1d3sw1ped/SteamCache2/vfs"
|
||||||
@@ -15,8 +17,6 @@ func testMemory() vfs.VFS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fast := testMemory()
|
fast := testMemory()
|
||||||
slow := testMemory()
|
slow := testMemory()
|
||||||
|
|
||||||
@@ -29,8 +29,6 @@ func TestNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewPanics(t *testing.T) {
|
func TestNewPanics(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r == nil {
|
if r := recover(); r == nil {
|
||||||
t.Fatal("expected panic but did not get one")
|
t.Fatal("expected panic but did not get one")
|
||||||
@@ -42,9 +40,7 @@ func TestNewPanics(t *testing.T) {
|
|||||||
cache.SetSlow(nil)
|
cache.SetSlow(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetAndGet(t *testing.T) {
|
func TestCreateAndOpen(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fast := testMemory()
|
fast := testMemory()
|
||||||
slow := testMemory()
|
slow := testMemory()
|
||||||
cache := New(nil)
|
cache := New(nil)
|
||||||
@@ -54,23 +50,26 @@ func TestSetAndGet(t *testing.T) {
|
|||||||
key := "test"
|
key := "test"
|
||||||
value := []byte("value")
|
value := []byte("value")
|
||||||
|
|
||||||
if err := cache.Set(key, value); err != nil {
|
w, err := cache.Create(key, int64(len(value)))
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := cache.Get(key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
rc, err := cache.Open(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
if string(got) != string(value) {
|
if string(got) != string(value) {
|
||||||
t.Fatalf("expected %s, got %s", value, got)
|
t.Fatalf("expected %s, got %s", value, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetAndGetNoFast(t *testing.T) {
|
func TestCreateAndOpenNoFast(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
slow := testMemory()
|
slow := testMemory()
|
||||||
cache := New(nil)
|
cache := New(nil)
|
||||||
cache.SetSlow(slow)
|
cache.SetSlow(slow)
|
||||||
@@ -78,22 +77,26 @@ func TestSetAndGetNoFast(t *testing.T) {
|
|||||||
key := "test"
|
key := "test"
|
||||||
value := []byte("value")
|
value := []byte("value")
|
||||||
|
|
||||||
if err := cache.Set(key, value); err != nil {
|
w, err := cache.Create(key, int64(len(value)))
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := cache.Get(key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
rc, err := cache.Open(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
if string(got) != string(value) {
|
if string(got) != string(value) {
|
||||||
t.Fatalf("expected %s, got %s", value, got)
|
t.Fatalf("expected %s, got %s", value, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestCaching(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
|
func TestCachingPromotion(t *testing.T) {
|
||||||
fast := testMemory()
|
fast := testMemory()
|
||||||
slow := testMemory()
|
slow := testMemory()
|
||||||
cache := New(func(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
|
cache := New(func(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
|
||||||
@@ -105,71 +108,42 @@ func TestCaching(t *testing.T) {
|
|||||||
key := "test"
|
key := "test"
|
||||||
value := []byte("value")
|
value := []byte("value")
|
||||||
|
|
||||||
if err := fast.Set(key, value); err != nil {
|
ws, _ := slow.Create(key, int64(len(value)))
|
||||||
t.Fatalf("unexpected error: %v", err)
|
ws.Write(value)
|
||||||
}
|
ws.Close()
|
||||||
|
|
||||||
if err := slow.Set(key, value); err != nil {
|
rc, err := cache.Open(key)
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, state, err := cache.GetS(key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if state != cachestate.CacheStateHit {
|
got, _ := io.ReadAll(rc)
|
||||||
t.Fatalf("expected %v, got %v", cachestate.CacheStateHit, state)
|
rc.Close()
|
||||||
}
|
|
||||||
|
|
||||||
err = fast.Delete(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, state, err := cache.GetS(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if state != cachestate.CacheStateMiss {
|
|
||||||
t.Fatalf("expected %v, got %v", cachestate.CacheStateMiss, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(got) != string(value) {
|
if string(got) != string(value) {
|
||||||
t.Fatalf("expected %s, got %s", value, got)
|
t.Fatalf("expected %s, got %s", value, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cache.Delete(key)
|
// Check if promoted to fast
|
||||||
|
_, err = fast.Open(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Error("Expected promotion to fast cache")
|
||||||
}
|
|
||||||
|
|
||||||
_, state, err = cache.GetS(key)
|
|
||||||
if !errors.Is(err, vfserror.ErrNotFound) {
|
|
||||||
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
|
|
||||||
}
|
|
||||||
if state != cachestate.CacheStateNotFound {
|
|
||||||
t.Fatalf("expected %v, got %v", cachestate.CacheStateNotFound, state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetNotFound(t *testing.T) {
|
func TestOpenNotFound(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fast := testMemory()
|
fast := testMemory()
|
||||||
slow := testMemory()
|
slow := testMemory()
|
||||||
cache := New(nil)
|
cache := New(nil)
|
||||||
cache.SetFast(fast)
|
cache.SetFast(fast)
|
||||||
cache.SetSlow(slow)
|
cache.SetSlow(slow)
|
||||||
|
|
||||||
_, err := cache.Get("nonexistent")
|
_, err := cache.Open("nonexistent")
|
||||||
if !errors.Is(err, vfserror.ErrNotFound) {
|
if !errors.Is(err, vfserror.ErrNotFound) {
|
||||||
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
|
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDelete(t *testing.T) {
|
func TestDelete(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fast := testMemory()
|
fast := testMemory()
|
||||||
slow := testMemory()
|
slow := testMemory()
|
||||||
cache := New(nil)
|
cache := New(nil)
|
||||||
@@ -179,23 +153,24 @@ func TestDelete(t *testing.T) {
|
|||||||
key := "test"
|
key := "test"
|
||||||
value := []byte("value")
|
value := []byte("value")
|
||||||
|
|
||||||
if err := cache.Set(key, value); err != nil {
|
w, err := cache.Create(key, int64(len(value)))
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
if err := cache.Delete(key); err != nil {
|
if err := cache.Delete(key); err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := cache.Get(key)
|
_, err = cache.Open(key)
|
||||||
if !errors.Is(err, vfserror.ErrNotFound) {
|
if !errors.Is(err, vfserror.ErrNotFound) {
|
||||||
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
|
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStat(t *testing.T) {
|
func TestStat(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fast := testMemory()
|
fast := testMemory()
|
||||||
slow := testMemory()
|
slow := testMemory()
|
||||||
cache := New(nil)
|
cache := New(nil)
|
||||||
@@ -205,9 +180,12 @@ func TestStat(t *testing.T) {
|
|||||||
key := "test"
|
key := "test"
|
||||||
value := []byte("value")
|
value := []byte("value")
|
||||||
|
|
||||||
if err := cache.Set(key, value); err != nil {
|
w, err := cache.Create(key, int64(len(value)))
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
info, err := cache.Stat(key)
|
info, err := cache.Stat(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,4 +195,7 @@ func TestStat(t *testing.T) {
|
|||||||
if info == nil {
|
if info == nil {
|
||||||
t.Fatal("expected file info to be non-nil")
|
t.Fatal("expected file info to be non-nil")
|
||||||
}
|
}
|
||||||
|
if info.Size() != int64(len(value)) {
|
||||||
|
t.Errorf("expected size %d, got %d", len(value), info.Size())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// vfs/cachestate/cachestate.go
|
||||||
package cachestate
|
package cachestate
|
||||||
|
|
||||||
import "s1d3sw1ped/SteamCache2/vfs/vfserror"
|
import "s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
|
|||||||
409
vfs/disk/disk.go
409
vfs/disk/disk.go
@@ -1,15 +1,52 @@
|
|||||||
|
// vfs/disk/disk.go
|
||||||
package disk
|
package disk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
"s1d3sw1ped/SteamCache2/vfs"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
diskCapacityBytes = promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "disk_cache_capacity_bytes",
|
||||||
|
Help: "Total capacity of the disk cache in bytes",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
diskSizeBytes = promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "disk_cache_size_bytes",
|
||||||
|
Help: "Total size of the disk cache in bytes",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
diskReadBytes = promauto.NewCounter(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "disk_cache_read_bytes_total",
|
||||||
|
Help: "Total number of bytes read from the disk cache",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
diskWriteBytes = promauto.NewCounter(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "disk_cache_write_bytes_total",
|
||||||
|
Help: "Total number of bytes written to the disk cache",
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure DiskFS implements VFS.
|
// Ensure DiskFS implements VFS.
|
||||||
@@ -21,8 +58,49 @@ type DiskFS struct {
|
|||||||
|
|
||||||
info map[string]*vfs.FileInfo
|
info map[string]*vfs.FileInfo
|
||||||
capacity int64
|
capacity int64
|
||||||
mu sync.Mutex
|
size int64
|
||||||
sg sync.WaitGroup
|
mu sync.RWMutex
|
||||||
|
keyLocks sync.Map // map[string]*sync.RWMutex
|
||||||
|
LRU *lruList
|
||||||
|
}
|
||||||
|
|
||||||
|
// lruList for LRU eviction
|
||||||
|
type lruList struct {
|
||||||
|
list *list.List
|
||||||
|
elem map[string]*list.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLruList() *lruList {
|
||||||
|
return &lruList{
|
||||||
|
list: list.New(),
|
||||||
|
elem: make(map[string]*list.Element),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) MoveToFront(key string) {
|
||||||
|
if e, ok := l.elem[key]; ok {
|
||||||
|
l.list.MoveToFront(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) Add(key string, fi *vfs.FileInfo) *list.Element {
|
||||||
|
e := l.list.PushFront(fi)
|
||||||
|
l.elem[key] = e
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) Remove(key string) {
|
||||||
|
if e, ok := l.elem[key]; ok {
|
||||||
|
l.list.Remove(e)
|
||||||
|
delete(l.elem, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) Back() *vfs.FileInfo {
|
||||||
|
if e := l.list.Back(); e != nil {
|
||||||
|
return e.Value.(*vfs.FileInfo)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new DiskFS.
|
// New creates a new DiskFS.
|
||||||
@@ -40,6 +118,11 @@ func new(root string, capacity int64, skipinit bool) *DiskFS {
|
|||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
panic(err) // panic if the error is something other than not found
|
panic(err) // panic if the error is something other than not found
|
||||||
}
|
}
|
||||||
|
os.Mkdir(root, 0755) // create the root directory if it does not exist
|
||||||
|
fi, err = os.Stat(root) // re-stat to get the file info
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // panic if the re-stat fails
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !fi.IsDir() {
|
if !fi.IsDir() {
|
||||||
panic("disk root must be a directory") // panic if the root is not a directory
|
panic("disk root must be a directory") // panic if the root is not a directory
|
||||||
@@ -49,14 +132,18 @@ func new(root string, capacity int64, skipinit bool) *DiskFS {
|
|||||||
root: root,
|
root: root,
|
||||||
info: make(map[string]*vfs.FileInfo),
|
info: make(map[string]*vfs.FileInfo),
|
||||||
capacity: capacity,
|
capacity: capacity,
|
||||||
mu: sync.Mutex{},
|
mu: sync.RWMutex{},
|
||||||
sg: sync.WaitGroup{},
|
keyLocks: sync.Map{},
|
||||||
|
LRU: newLruList(),
|
||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll(dfs.root, 0755)
|
os.MkdirAll(dfs.root, 0755)
|
||||||
|
|
||||||
|
diskCapacityBytes.Set(float64(dfs.capacity))
|
||||||
|
|
||||||
if !skipinit {
|
if !skipinit {
|
||||||
dfs.init()
|
dfs.init()
|
||||||
|
diskSizeBytes.Set(float64(dfs.Size()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return dfs
|
return dfs
|
||||||
@@ -71,48 +158,39 @@ func NewSkipInit(root string, capacity int64) *DiskFS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiskFS) init() {
|
func (d *DiskFS) init() {
|
||||||
// logger.Logger.Info().Str("name", d.Name()).Str("root", d.root).Str("capacity", units.HumanSize(float64(d.capacity))).Msg("init")
|
|
||||||
|
|
||||||
tstart := time.Now()
|
tstart := time.Now()
|
||||||
|
|
||||||
d.walk(d.root)
|
err := filepath.Walk(d.root, func(npath string, info os.FileInfo, err error) error {
|
||||||
d.sg.Wait()
|
|
||||||
|
|
||||||
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) {
|
|
||||||
d.sg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer d.sg.Done()
|
|
||||||
filepath.Walk(path, func(npath string, info os.FileInfo, err error) error {
|
|
||||||
if path == npath {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
d.walk(npath)
|
return nil
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
k := npath[len(d.root)+1:]
|
k := strings.ReplaceAll(npath[len(d.root)+1:], "\\", "/")
|
||||||
d.info[k] = vfs.NewFileInfoFromOS(info, k)
|
fi := vfs.NewFileInfoFromOS(info, k)
|
||||||
|
d.info[k] = fi
|
||||||
|
d.LRU.Add(k, fi)
|
||||||
|
d.size += info.Size()
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
// logger.Logger.Debug().Str("name", d.Name()).Str("root", d.root).Str("capacity", units.HumanSize(float64(d.capacity))).Str("path", npath).Msg("init")
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}()
|
if err != nil {
|
||||||
|
logger.Logger.Error().Err(err).Msg("Walk failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiskFS) Capacity() int64 {
|
func (d *DiskFS) Capacity() int64 {
|
||||||
@@ -124,108 +202,259 @@ func (d *DiskFS) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiskFS) Size() int64 {
|
func (d *DiskFS) Size() int64 {
|
||||||
var size int64
|
d.mu.RLock()
|
||||||
d.mu.Lock()
|
defer d.mu.RUnlock()
|
||||||
defer d.mu.Unlock()
|
return d.size
|
||||||
for _, v := range d.info {
|
|
||||||
size += v.Size()
|
|
||||||
}
|
|
||||||
return size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiskFS) Set(key string, src []byte) error {
|
func (d *DiskFS) getKeyLock(key string) *sync.RWMutex {
|
||||||
|
mu, _ := d.keyLocks.LoadOrStore(key, &sync.RWMutex{})
|
||||||
|
return mu.(*sync.RWMutex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiskFS) Create(key string, size int64) (io.WriteCloser, error) {
|
||||||
|
if key == "" {
|
||||||
|
return nil, vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
if key[0] == '/' {
|
||||||
|
return nil, vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize key to prevent path traversal
|
||||||
|
key = filepath.Clean(key)
|
||||||
|
key = strings.ReplaceAll(key, "\\", "/") // Ensure forward slashes for consistency
|
||||||
|
if strings.Contains(key, "..") {
|
||||||
|
return nil, vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mu.RLock()
|
||||||
if d.capacity > 0 {
|
if d.capacity > 0 {
|
||||||
if size := d.Size() + int64(len(src)); size > d.capacity {
|
if d.size+size > d.capacity {
|
||||||
return vfserror.ErrDiskFull
|
d.mu.RUnlock()
|
||||||
|
return nil, vfserror.ErrDiskFull
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
d.mu.RUnlock()
|
||||||
|
|
||||||
if _, err := d.Stat(key); err == nil {
|
keyMu := d.getKeyLock(key)
|
||||||
d.Delete(key)
|
keyMu.Lock()
|
||||||
}
|
defer keyMu.Unlock()
|
||||||
|
|
||||||
|
// Check again after lock
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
if fi, exists := d.info[key]; exists {
|
||||||
os.MkdirAll(filepath.Join(d.root, filepath.Dir(key)), 0755)
|
d.size -= fi.Size()
|
||||||
if err := os.WriteFile(filepath.Join(d.root, key), src, 0644); err != nil {
|
d.LRU.Remove(key)
|
||||||
|
delete(d.info, key)
|
||||||
|
path := filepath.Join(d.root, key)
|
||||||
|
os.Remove(path) // Ignore error, as file might not exist or other issues
|
||||||
|
}
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
path := filepath.Join(d.root, key)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/") // Ensure forward slashes for consistency
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &diskWriteCloser{
|
||||||
|
Writer: file,
|
||||||
|
onClose: func(n int64) error {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fi, err := os.Stat(filepath.Join(d.root, key))
|
d.mu.Lock()
|
||||||
if err != nil {
|
finfo := vfs.NewFileInfoFromOS(fi, key)
|
||||||
panic(err)
|
d.info[key] = finfo
|
||||||
}
|
d.LRU.Add(key, finfo)
|
||||||
|
d.size += n
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
d.info[key] = vfs.NewFileInfoFromOS(fi, key)
|
diskWriteBytes.Add(float64(n))
|
||||||
|
diskSizeBytes.Set(float64(d.Size()))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
},
|
||||||
|
key: key,
|
||||||
|
file: file,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type diskWriteCloser struct {
|
||||||
|
io.Writer
|
||||||
|
onClose func(int64) error
|
||||||
|
n int64
|
||||||
|
key string
|
||||||
|
file *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *diskWriteCloser) Write(p []byte) (int, error) {
|
||||||
|
n, err := wc.Writer.Write(p)
|
||||||
|
wc.n += int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *diskWriteCloser) Close() error {
|
||||||
|
err := wc.file.Close()
|
||||||
|
if e := wc.onClose(wc.n); e != nil {
|
||||||
|
os.Remove(wc.file.Name())
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes the value of key.
|
// Delete deletes the value of key.
|
||||||
func (d *DiskFS) Delete(key string) error {
|
func (d *DiskFS) Delete(key string) error {
|
||||||
_, err := d.Stat(key)
|
if key == "" {
|
||||||
if err != nil {
|
return vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
if key[0] == '/' {
|
||||||
|
return vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize key to prevent path traversal
|
||||||
|
key = filepath.Clean(key)
|
||||||
|
key = strings.ReplaceAll(key, "\\", "/") // Ensure forward slashes for consistency
|
||||||
|
if strings.Contains(key, "..") {
|
||||||
|
return vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
keyMu := d.getKeyLock(key)
|
||||||
|
keyMu.Lock()
|
||||||
|
defer keyMu.Unlock()
|
||||||
|
|
||||||
|
d.mu.Lock()
|
||||||
|
fi, exists := d.info[key]
|
||||||
|
if !exists {
|
||||||
|
d.mu.Unlock()
|
||||||
|
return vfserror.ErrNotFound
|
||||||
|
}
|
||||||
|
d.size -= fi.Size()
|
||||||
|
d.LRU.Remove(key)
|
||||||
|
delete(d.info, key)
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
path := filepath.Join(d.root, key)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/") // Ensure forward slashes for consistency
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mu.Lock()
|
diskSizeBytes.Set(float64(d.Size()))
|
||||||
defer d.mu.Unlock()
|
|
||||||
delete(d.info, key)
|
|
||||||
if err := os.Remove(filepath.Join(d.root, key)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets the value of key and returns it.
|
// Open opens the file at key and returns it.
|
||||||
func (d *DiskFS) Get(key string) ([]byte, error) {
|
func (d *DiskFS) Open(key string) (io.ReadCloser, error) {
|
||||||
_, err := d.Stat(key)
|
if key == "" {
|
||||||
if err != nil {
|
return nil, vfserror.ErrInvalidKey
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
if key[0] == '/' {
|
||||||
|
return nil, vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize key to prevent path traversal
|
||||||
|
key = filepath.Clean(key)
|
||||||
|
key = strings.ReplaceAll(key, "\\", "/") // Ensure forward slashes for consistency
|
||||||
|
if strings.Contains(key, "..") {
|
||||||
|
return nil, vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
keyMu := d.getKeyLock(key)
|
||||||
|
keyMu.RLock()
|
||||||
|
defer keyMu.RUnlock()
|
||||||
|
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
fi, exists := d.info[key]
|
||||||
|
if !exists {
|
||||||
|
d.mu.Unlock()
|
||||||
|
return nil, vfserror.ErrNotFound
|
||||||
|
}
|
||||||
|
fi.ATime = time.Now()
|
||||||
|
d.LRU.MoveToFront(key)
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(d.root, key))
|
path := filepath.Join(d.root, key)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/") // Ensure forward slashes for consistency
|
||||||
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
// Update metrics on close
|
||||||
|
return &readCloser{
|
||||||
|
ReadCloser: file,
|
||||||
|
onClose: func(n int64) {
|
||||||
|
diskReadBytes.Add(float64(n))
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type readCloser struct {
|
||||||
|
io.ReadCloser
|
||||||
|
onClose func(int64)
|
||||||
|
n int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *readCloser) Read(p []byte) (int, error) {
|
||||||
|
n, err := rc.ReadCloser.Read(p)
|
||||||
|
rc.n += int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *readCloser) Close() error {
|
||||||
|
err := rc.ReadCloser.Close()
|
||||||
|
rc.onClose(rc.n)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// 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) {
|
func (d *DiskFS) Stat(key string) (*vfs.FileInfo, error) {
|
||||||
d.mu.Lock()
|
if key == "" {
|
||||||
fi, ok := d.info[key]
|
return nil, vfserror.ErrInvalidKey
|
||||||
d.mu.Unlock() // unlock before statting the file
|
}
|
||||||
|
if key[0] == '/' {
|
||||||
|
return nil, vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
if !ok {
|
// Sanitize key to prevent path traversal
|
||||||
fii, err := os.Stat(filepath.Join(d.root, key))
|
key = filepath.Clean(key)
|
||||||
if err != nil {
|
key = strings.ReplaceAll(key, "\\", "/") // Ensure forward slashes for consistency
|
||||||
|
if strings.Contains(key, "..") {
|
||||||
|
return nil, vfserror.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
keyMu := d.getKeyLock(key)
|
||||||
|
keyMu.RLock()
|
||||||
|
defer keyMu.RUnlock()
|
||||||
|
|
||||||
|
d.mu.RLock()
|
||||||
|
defer d.mu.RUnlock()
|
||||||
|
|
||||||
|
if fi, ok := d.info[key]; !ok {
|
||||||
return nil, vfserror.ErrNotFound
|
return nil, vfserror.ErrNotFound
|
||||||
}
|
} else {
|
||||||
|
|
||||||
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
|
return fi, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DiskFS) StatAll() []*vfs.FileInfo {
|
func (d *DiskFS) StatAll() []*vfs.FileInfo {
|
||||||
m.mu.Lock()
|
d.mu.RLock()
|
||||||
defer m.mu.Unlock()
|
defer d.mu.RUnlock()
|
||||||
|
|
||||||
// hard copy the file info to prevent modification of the original file info or the other way around
|
// hard copy the file info to prevent modification of the original file info or the other way around
|
||||||
files := make([]*vfs.FileInfo, 0, len(m.info))
|
files := make([]*vfs.FileInfo, 0, len(d.info))
|
||||||
for _, v := range m.info {
|
for _, v := range d.info {
|
||||||
fi := *v
|
fi := *v
|
||||||
files = append(files, &fi)
|
files = append(files, &fi)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,181 @@
|
|||||||
|
// vfs/disk/disk_test.go
|
||||||
package disk
|
package disk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAllDisk(t *testing.T) {
|
func TestCreateAndOpen(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
m := NewSkipInit(t.TempDir(), 1024)
|
m := NewSkipInit(t.TempDir(), 1024)
|
||||||
if err := m.Set("key", []byte("value")); err != nil {
|
key := "key"
|
||||||
t.Errorf("Set failed: %v", err)
|
value := []byte("value")
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Set("key", []byte("value1")); err != nil {
|
w, err := m.Create(key, int64(len(value)))
|
||||||
t.Errorf("Set failed: %v", err)
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
}
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
if d, err := m.Get("key"); err != nil {
|
rc, err := m.Open(key)
|
||||||
t.Errorf("Get failed: %v", err)
|
if err != nil {
|
||||||
} else if string(d) != "value1" {
|
t.Fatalf("Open failed: %v", err)
|
||||||
t.Errorf("Get failed: got %s, want %s", d, "value1")
|
|
||||||
}
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
if err := m.Delete("key"); err != nil {
|
if string(got) != string(value) {
|
||||||
t.Errorf("Delete failed: %v", err)
|
t.Fatalf("expected %s, got %s", value, got)
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := m.Get("key"); err == nil {
|
|
||||||
t.Errorf("Get failed: got nil, want %v", vfserror.ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Delete("key"); err == nil {
|
|
||||||
t.Errorf("Delete failed: got nil, want %v", vfserror.ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := m.Stat("key"); err == nil {
|
|
||||||
t.Errorf("Stat failed: got nil, want %v", vfserror.ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Set("key", []byte("value")); err != nil {
|
|
||||||
t.Errorf("Set failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := m.Stat("key"); err != nil {
|
|
||||||
t.Errorf("Stat failed: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLimited(t *testing.T) {
|
func TestOverwrite(t *testing.T) {
|
||||||
t.Parallel()
|
m := NewSkipInit(t.TempDir(), 1024)
|
||||||
|
key := "key"
|
||||||
|
value1 := []byte("value1")
|
||||||
|
value2 := []byte("value2")
|
||||||
|
|
||||||
|
w, err := m.Create(key, int64(len(value1)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(value1)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
w, err = m.Create(key, int64(len(value2)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(value2)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
rc, err := m.Open(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
|
if string(got) != string(value2) {
|
||||||
|
t.Fatalf("expected %s, got %s", value2, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
m := NewSkipInit(t.TempDir(), 1024)
|
||||||
|
key := "key"
|
||||||
|
value := []byte("value")
|
||||||
|
|
||||||
|
w, err := m.Create(key, int64(len(value)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
if err := m.Delete(key); err != nil {
|
||||||
|
t.Fatalf("Delete failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = m.Open(key)
|
||||||
|
if !errors.Is(err, vfserror.ErrNotFound) {
|
||||||
|
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapacityLimit(t *testing.T) {
|
||||||
m := NewSkipInit(t.TempDir(), 10)
|
m := NewSkipInit(t.TempDir(), 10)
|
||||||
for i := 0; i < 11; i++ {
|
for i := 0; i < 11; i++ {
|
||||||
if err := m.Set(fmt.Sprintf("key%d", i), []byte("1")); err != nil && i < 10 {
|
w, err := m.Create(fmt.Sprintf("key%d", i), 1)
|
||||||
t.Errorf("Set failed: %v", err)
|
if err != nil && i < 10 {
|
||||||
|
t.Errorf("Create failed: %v", err)
|
||||||
} else if i == 10 && err == nil {
|
} else if i == 10 && err == nil {
|
||||||
t.Errorf("Set succeeded: got nil, want %v", vfserror.ErrDiskFull)
|
t.Errorf("Create succeeded: got nil, want %v", vfserror.ErrDiskFull)
|
||||||
|
}
|
||||||
|
if i < 10 {
|
||||||
|
w.Write([]byte("1"))
|
||||||
|
w.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInit(t *testing.T) {
|
func TestInitExistingFiles(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
td := t.TempDir()
|
td := t.TempDir()
|
||||||
|
|
||||||
path := filepath.Join(td, "test", "key")
|
path := filepath.Join(td, "test", "key")
|
||||||
|
|
||||||
os.MkdirAll(filepath.Dir(path), 0755)
|
os.MkdirAll(filepath.Dir(path), 0755)
|
||||||
|
|
||||||
os.WriteFile(path, []byte("value"), 0644)
|
os.WriteFile(path, []byte("value"), 0644)
|
||||||
|
|
||||||
m := New(td, 10)
|
m := New(td, 10)
|
||||||
if _, err := m.Get("test/key"); err != nil {
|
rc, err := m.Open("test/key")
|
||||||
t.Errorf("Get failed: %v", err)
|
if err != nil {
|
||||||
|
t.Fatalf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
|
if string(got) != "value" {
|
||||||
|
t.Errorf("expected value, got %s", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, _ := m.Stat("test/key")
|
s, err := m.Stat("test/key")
|
||||||
if s.Name() != "test/key" {
|
if err != nil {
|
||||||
t.Errorf("Stat failed: got %s, want %s", s.Name(), "key")
|
t.Fatalf("Stat failed: %v", err)
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
t.Error("Stat returned nil")
|
||||||
|
}
|
||||||
|
if s != nil && s.Name() != "test/key" {
|
||||||
|
t.Errorf("Stat failed: got %s, want %s", s.Name(), "test/key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSizeConsistency(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
os.WriteFile(filepath.Join(td, "key2"), []byte("value2"), 0644)
|
||||||
|
|
||||||
|
m := New(td, 1024)
|
||||||
|
if m.Size() != 6 {
|
||||||
|
t.Errorf("Size failed: got %d, want 6", m.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := m.Create("key", 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write([]byte("value"))
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
w, err = m.Create("key1", 6)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write([]byte("value1"))
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
assumedSize := int64(6 + 5 + 6)
|
||||||
|
if assumedSize != m.Size() {
|
||||||
|
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := m.Open("key")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
d, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
if string(d) != "value" {
|
||||||
|
t.Errorf("Get failed: got %s, want value", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
m = New(td, 1024)
|
||||||
|
if assumedSize != m.Size() {
|
||||||
|
t.Errorf("Size failed: got %d, want %d", m.Size(), assumedSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// vfs/fileinfo.go
|
||||||
package vfs
|
package vfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,9 +13,9 @@ type FileInfo struct {
|
|||||||
ATime time.Time
|
ATime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileInfo(name string, size int64, modTime time.Time) *FileInfo {
|
func NewFileInfo(key string, size int64, modTime time.Time) *FileInfo {
|
||||||
return &FileInfo{
|
return &FileInfo{
|
||||||
name: name,
|
name: key,
|
||||||
size: size,
|
size: size,
|
||||||
MTime: modTime,
|
MTime: modTime,
|
||||||
ATime: time.Now(),
|
ATime: time.Now(),
|
||||||
|
|||||||
118
vfs/gc/gc.go
118
vfs/gc/gc.go
@@ -1,13 +1,79 @@
|
|||||||
|
// vfs/gc/gc.go
|
||||||
package gc
|
package gc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
"s1d3sw1ped/SteamCache2/vfs"
|
||||||
|
"s1d3sw1ped/SteamCache2/vfs/cachestate"
|
||||||
|
"s1d3sw1ped/SteamCache2/vfs/disk"
|
||||||
|
"s1d3sw1ped/SteamCache2/vfs/memory"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LRUGC deletes files in LRU order until enough space is reclaimed.
|
||||||
|
func LRUGC(vfss vfs.VFS, size uint) {
|
||||||
|
attempts := 0
|
||||||
|
deletions := 0
|
||||||
|
var reclaimed uint
|
||||||
|
|
||||||
|
for reclaimed < size {
|
||||||
|
if attempts > 10 {
|
||||||
|
logger.Logger.Debug().
|
||||||
|
Int("attempts", attempts).
|
||||||
|
Msg("GC: Too many attempts to reclaim space, giving up")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
switch fs := vfss.(type) {
|
||||||
|
case *disk.DiskFS:
|
||||||
|
fi := fs.LRU.Back()
|
||||||
|
if fi == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sz := uint(fi.Size())
|
||||||
|
err := fs.Delete(fi.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reclaimed += sz
|
||||||
|
deletions++
|
||||||
|
case *memory.MemoryFS:
|
||||||
|
fi := fs.LRU.Back()
|
||||||
|
if fi == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sz := uint(fi.Size())
|
||||||
|
err := fs.Delete(fi.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reclaimed += sz
|
||||||
|
deletions++
|
||||||
|
default:
|
||||||
|
// Fallback to old method if not supported
|
||||||
|
stats := vfss.StatAll()
|
||||||
|
if len(stats) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fi := stats[0] // Assume sorted or pick first
|
||||||
|
sz := uint(fi.Size())
|
||||||
|
err := vfss.Delete(fi.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reclaimed += sz
|
||||||
|
deletions++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PromotionDecider(fi *vfs.FileInfo, cs cachestate.CacheState) bool {
|
||||||
|
return time.Since(fi.AccessTime()) < time.Second*60 // Put hot files in the fast vfs if equipped
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure GCFS implements VFS.
|
// Ensure GCFS implements VFS.
|
||||||
var _ vfs.VFS = (*GCFS)(nil)
|
var _ vfs.VFS = (*GCFS)(nil)
|
||||||
|
|
||||||
@@ -18,14 +84,10 @@ type GCFS struct {
|
|||||||
|
|
||||||
// protected by mu
|
// protected by mu
|
||||||
gcHanderFunc GCHandlerFunc
|
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.
|
// 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 uint)
|
||||||
|
|
||||||
func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
|
func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
|
||||||
if multiplier <= 0 {
|
if multiplier <= 0 {
|
||||||
@@ -38,47 +100,17 @@ func New(vfs vfs.VFS, multiplier int, gcHandlerFunc GCHandlerFunc) *GCFS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats returns the lifetime bytes, lifetime files, reclaimed bytes and deleted files.
|
// Create overrides the Create method of the VFS interface. It tries to create the key, if it fails due to disk full error, it calls the GC handler and tries again. If it still fails it returns the error.
|
||||||
// The lifetime bytes and lifetime files are the total bytes and files that have been freed up by the GC handler.
|
func (g *GCFS) Create(key string, size int64) (io.WriteCloser, error) {
|
||||||
// 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.
|
w, err := g.VFS.Create(key, size) // try to create the key
|
||||||
// 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 it fails due to disk full error, call the GC handler and try again in a loop that will continue until it succeeds or the error is not disk full
|
||||||
if err == vfserror.ErrDiskFull && g.gcHanderFunc != nil { // if the error is disk full and there is a GC handler
|
if err == vfserror.ErrDiskFull && g.gcHanderFunc != nil { // if the error is disk full and there is a GC handler
|
||||||
tstart := time.Now()
|
g.gcHanderFunc(g.VFS, uint(size*int64(g.multiplier))) // call the GC handler
|
||||||
reclaimedBytes, deletedFiles := g.gcHanderFunc(g.VFS, uint(len(src)*g.multiplier)) // call the GC handler
|
w, err = g.VFS.Create(key, size)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return w, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GCFS) Name() string {
|
func (g *GCFS) Name() string {
|
||||||
|
|||||||
@@ -1,105 +1,73 @@
|
|||||||
|
// vfs/gc/gc_test.go
|
||||||
package gc
|
package gc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs/memory"
|
"s1d3sw1ped/SteamCache2/vfs/memory"
|
||||||
"sort"
|
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/exp/rand"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGCSmallRandom(t *testing.T) {
|
func TestGCOnFull(t *testing.T) {
|
||||||
t.Parallel()
|
m := memory.New(10)
|
||||||
|
gc := New(m, 2, LRUGC)
|
||||||
|
|
||||||
m := memory.New(1024 * 1024 * 16)
|
for i := 0; i < 5; i++ {
|
||||||
gc := New(m, 10, func(vfs vfs.VFS, size uint) (uint, uint) {
|
w, err := gc.Create(fmt.Sprintf("key%d", i), 2)
|
||||||
deletions := 0
|
|
||||||
var reclaimed uint
|
|
||||||
|
|
||||||
t.Logf("GC starting to reclaim %d bytes", size)
|
|
||||||
|
|
||||||
stats := vfs.StatAll()
|
|
||||||
sort.Slice(stats, func(i, j int) bool {
|
|
||||||
// Sort by access time so we can remove the oldest files first.
|
|
||||||
return stats[i].AccessTime().Before(stats[j].AccessTime())
|
|
||||||
})
|
|
||||||
|
|
||||||
// Delete the oldest files until we've reclaimed enough space.
|
|
||||||
for _, s := range stats {
|
|
||||||
sz := uint(s.Size()) // Get the size of the file
|
|
||||||
err := vfs.Delete(s.Name())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
t.Fatalf("Create failed: %v", err)
|
||||||
}
|
|
||||||
reclaimed += sz // Track how much space we've reclaimed
|
|
||||||
deletions++ // Track how many files we've deleted
|
|
||||||
|
|
||||||
// t.Logf("GC deleting %s, %v", s.Name(), s.AccessTime().Format(time.RFC3339Nano))
|
|
||||||
|
|
||||||
if reclaimed >= size { // We've reclaimed enough space
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uint(reclaimed), uint(deletions)
|
|
||||||
})
|
|
||||||
|
|
||||||
for i := 0; i < 10000; i++ {
|
|
||||||
if err := gc.Set(fmt.Sprintf("key:%d", i), genRandomData(1024*1, 1024*4)); err != nil {
|
|
||||||
t.Errorf("Set failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
w.Write([]byte("ab"))
|
||||||
|
w.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if gc.Size() > 1024*1024*16 {
|
// Cache full at 10 bytes
|
||||||
t.Errorf("MemoryFS size is %d, want <= 1024", m.Size())
|
w, err := gc.Create("key5", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write([]byte("cd"))
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
if gc.Size() > 10 {
|
||||||
|
t.Errorf("Size exceeded: %d > 10", gc.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if older keys were evicted
|
||||||
|
_, err = m.Open("key0")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected key0 to be evicted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func genRandomData(min int, max int) []byte {
|
func TestNoGCNeeded(t *testing.T) {
|
||||||
data := make([]byte, rand.Intn(max-min)+min)
|
m := memory.New(20)
|
||||||
rand.Read(data)
|
gc := New(m, 2, LRUGC)
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGCLargeRandom(t *testing.T) {
|
for i := 0; i < 5; i++ {
|
||||||
t.Parallel()
|
w, err := gc.Create(fmt.Sprintf("key%d", i), 2)
|
||||||
|
if err != nil {
|
||||||
m := memory.New(1024 * 1024 * 16) // 16MB
|
t.Fatalf("Create failed: %v", err)
|
||||||
gc := New(m, 10, func(vfs vfs.VFS, size uint) (uint, uint) {
|
|
||||||
deletions := 0
|
|
||||||
var reclaimed uint
|
|
||||||
|
|
||||||
t.Logf("GC starting to reclaim %d bytes", size)
|
|
||||||
|
|
||||||
stats := vfs.StatAll()
|
|
||||||
sort.Slice(stats, func(i, j int) bool {
|
|
||||||
// Sort by access time so we can remove the oldest files first.
|
|
||||||
return stats[i].AccessTime().Before(stats[j].AccessTime())
|
|
||||||
})
|
|
||||||
|
|
||||||
// Delete the oldest files until we've reclaimed enough space.
|
|
||||||
for _, s := range stats {
|
|
||||||
sz := uint(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
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
w.Write([]byte("ab"))
|
||||||
|
w.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return uint(reclaimed), uint(deletions)
|
if gc.Size() != 10 {
|
||||||
})
|
t.Errorf("Size: got %d, want 10", gc.Size())
|
||||||
|
}
|
||||||
for i := 0; i < 10000; i++ {
|
}
|
||||||
if err := gc.Set(fmt.Sprintf("key:%d", i), genRandomData(1024, 1024*1024)); err != nil {
|
|
||||||
t.Errorf("Set failed: %v", err)
|
func TestGCInsufficientSpace(t *testing.T) {
|
||||||
}
|
m := memory.New(5)
|
||||||
}
|
gc := New(m, 1, LRUGC)
|
||||||
|
|
||||||
if gc.Size() > 1024*1024*16 {
|
w, err := gc.Create("key0", 10)
|
||||||
t.Errorf("MemoryFS size is %d, want <= 1024", m.Size())
|
if err == nil {
|
||||||
|
w.Close()
|
||||||
|
t.Error("Expected ErrDiskFull")
|
||||||
|
} else if !errors.Is(err, vfserror.ErrDiskFull) {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,49 @@
|
|||||||
|
// vfs/memory/memory.go
|
||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"io"
|
||||||
|
"s1d3sw1ped/SteamCache2/steamcache/logger"
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
"s1d3sw1ped/SteamCache2/vfs"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
memoryCapacityBytes = promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "memory_cache_capacity_bytes",
|
||||||
|
Help: "Total capacity of the memory cache in bytes",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
memorySizeBytes = promauto.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "memory_cache_size_bytes",
|
||||||
|
Help: "Total size of the memory cache in bytes",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
memoryReadBytes = promauto.NewCounter(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "memory_cache_read_bytes_total",
|
||||||
|
Help: "Total number of bytes read from the memory cache",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
memoryWriteBytes = promauto.NewCounter(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "memory_cache_write_bytes_total",
|
||||||
|
Help: "Total number of bytes written to the memory cache",
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure MemoryFS implements VFS.
|
// Ensure MemoryFS implements VFS.
|
||||||
@@ -20,7 +59,49 @@ type file struct {
|
|||||||
type MemoryFS struct {
|
type MemoryFS struct {
|
||||||
files map[string]*file
|
files map[string]*file
|
||||||
capacity int64
|
capacity int64
|
||||||
mu sync.Mutex
|
size int64
|
||||||
|
mu sync.RWMutex
|
||||||
|
keyLocks sync.Map // map[string]*sync.RWMutex
|
||||||
|
LRU *lruList
|
||||||
|
}
|
||||||
|
|
||||||
|
// lruList for LRU eviction
|
||||||
|
type lruList struct {
|
||||||
|
list *list.List
|
||||||
|
elem map[string]*list.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLruList() *lruList {
|
||||||
|
return &lruList{
|
||||||
|
list: list.New(),
|
||||||
|
elem: make(map[string]*list.Element),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) MoveToFront(key string) {
|
||||||
|
if e, ok := l.elem[key]; ok {
|
||||||
|
l.list.MoveToFront(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) Add(key string, fi *vfs.FileInfo) *list.Element {
|
||||||
|
e := l.list.PushFront(fi)
|
||||||
|
l.elem[key] = e
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) Remove(key string) {
|
||||||
|
if e, ok := l.elem[key]; ok {
|
||||||
|
l.list.Remove(e)
|
||||||
|
delete(l.elem, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lruList) Back() *vfs.FileInfo {
|
||||||
|
if e := l.list.Back(); e != nil {
|
||||||
|
return e.Value.(*vfs.FileInfo)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new MemoryFS.
|
// New creates a new MemoryFS.
|
||||||
@@ -29,11 +110,23 @@ func New(capacity int64) *MemoryFS {
|
|||||||
panic("memory capacity must be greater than 0") // panic if the capacity is less than or equal to 0
|
panic("memory capacity must be greater than 0") // panic if the capacity is less than or equal to 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return &MemoryFS{
|
logger.Logger.Info().
|
||||||
|
Str("name", "MemoryFS").
|
||||||
|
Str("capacity", units.HumanSize(float64(capacity))).
|
||||||
|
Msg("init")
|
||||||
|
|
||||||
|
mfs := &MemoryFS{
|
||||||
files: make(map[string]*file),
|
files: make(map[string]*file),
|
||||||
capacity: capacity,
|
capacity: capacity,
|
||||||
mu: sync.Mutex{},
|
mu: sync.RWMutex{},
|
||||||
|
keyLocks: sync.Map{},
|
||||||
|
LRU: newLruList(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memoryCapacityBytes.Set(float64(capacity))
|
||||||
|
memorySizeBytes.Set(float64(mfs.Size()))
|
||||||
|
|
||||||
|
return mfs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MemoryFS) Capacity() int64 {
|
func (m *MemoryFS) Capacity() int64 {
|
||||||
@@ -45,74 +138,118 @@ func (m *MemoryFS) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MemoryFS) Size() int64 {
|
func (m *MemoryFS) Size() int64 {
|
||||||
var size int64
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
m.mu.Lock()
|
return m.size
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
for _, v := range m.files {
|
|
||||||
size += int64(len(v.data))
|
|
||||||
}
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MemoryFS) Set(key string, src []byte) error {
|
func (m *MemoryFS) getKeyLock(key string) *sync.RWMutex {
|
||||||
|
mu, _ := m.keyLocks.LoadOrStore(key, &sync.RWMutex{})
|
||||||
|
return mu.(*sync.RWMutex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemoryFS) Create(key string, size int64) (io.WriteCloser, error) {
|
||||||
|
m.mu.RLock()
|
||||||
if m.capacity > 0 {
|
if m.capacity > 0 {
|
||||||
if size := m.Size() + int64(len(src)); size > m.capacity {
|
if m.size+size > m.capacity {
|
||||||
return vfserror.ErrDiskFull
|
m.mu.RUnlock()
|
||||||
|
return nil, vfserror.ErrDiskFull
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
keyMu := m.getKeyLock(key)
|
||||||
|
keyMu.Lock()
|
||||||
|
defer keyMu.Unlock()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
return &memWriteCloser{
|
||||||
|
Writer: buf,
|
||||||
|
onClose: func() error {
|
||||||
|
data := buf.Bytes()
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
if f, exists := m.files[key]; exists {
|
||||||
|
m.size -= int64(len(f.data))
|
||||||
m.files[key] = &file{
|
m.LRU.Remove(key)
|
||||||
fileinfo: vfs.NewFileInfo(
|
|
||||||
key,
|
|
||||||
int64(len(src)),
|
|
||||||
time.Now(),
|
|
||||||
),
|
|
||||||
data: make([]byte, len(src)),
|
|
||||||
}
|
}
|
||||||
copy(m.files[key].data, src)
|
fi := vfs.NewFileInfo(key, int64(len(data)), time.Now())
|
||||||
|
m.files[key] = &file{
|
||||||
|
fileinfo: fi,
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
m.LRU.Add(key, fi)
|
||||||
|
m.size += int64(len(data))
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
memoryWriteBytes.Add(float64(len(data)))
|
||||||
|
memorySizeBytes.Set(float64(m.Size()))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type memWriteCloser struct {
|
||||||
|
io.Writer
|
||||||
|
onClose func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *memWriteCloser) Close() error {
|
||||||
|
return wc.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MemoryFS) Delete(key string) error {
|
func (m *MemoryFS) Delete(key string) error {
|
||||||
_, err := m.Stat(key)
|
keyMu := m.getKeyLock(key)
|
||||||
if err != nil {
|
keyMu.Lock()
|
||||||
return err
|
defer keyMu.Unlock()
|
||||||
}
|
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
f, exists := m.files[key]
|
||||||
|
if !exists {
|
||||||
|
m.mu.Unlock()
|
||||||
|
return vfserror.ErrNotFound
|
||||||
|
}
|
||||||
|
m.size -= int64(len(f.data))
|
||||||
|
m.LRU.Remove(key)
|
||||||
delete(m.files, key)
|
delete(m.files, key)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
memorySizeBytes.Set(float64(m.Size()))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MemoryFS) Get(key string) ([]byte, error) {
|
func (m *MemoryFS) Open(key string) (io.ReadCloser, error) {
|
||||||
_, err := m.Stat(key)
|
keyMu := m.getKeyLock(key)
|
||||||
if err != nil {
|
keyMu.RLock()
|
||||||
return nil, err
|
defer keyMu.RUnlock()
|
||||||
}
|
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
f, exists := m.files[key]
|
||||||
|
if !exists {
|
||||||
|
m.mu.Unlock()
|
||||||
|
return nil, vfserror.ErrNotFound
|
||||||
|
}
|
||||||
|
f.fileinfo.ATime = time.Now()
|
||||||
|
m.LRU.MoveToFront(key)
|
||||||
|
dataCopy := make([]byte, len(f.data))
|
||||||
|
copy(dataCopy, f.data)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
m.files[key].fileinfo.ATime = time.Now()
|
memoryReadBytes.Add(float64(len(dataCopy)))
|
||||||
dst := make([]byte, len(m.files[key].data))
|
memorySizeBytes.Set(float64(m.Size()))
|
||||||
copy(dst, m.files[key].data)
|
|
||||||
|
|
||||||
return dst, nil
|
return io.NopCloser(bytes.NewReader(dataCopy)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MemoryFS) Stat(key string) (*vfs.FileInfo, error) {
|
func (m *MemoryFS) Stat(key string) (*vfs.FileInfo, error) {
|
||||||
m.mu.Lock()
|
keyMu := m.getKeyLock(key)
|
||||||
defer m.mu.Unlock()
|
keyMu.RLock()
|
||||||
|
defer keyMu.RUnlock()
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
f, ok := m.files[key]
|
f, ok := m.files[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -123,8 +260,8 @@ func (m *MemoryFS) Stat(key string) (*vfs.FileInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MemoryFS) StatAll() []*vfs.FileInfo {
|
func (m *MemoryFS) StatAll() []*vfs.FileInfo {
|
||||||
m.mu.Lock()
|
m.mu.RLock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
// hard copy the file info to prevent modification of the original file info or the other way around
|
// hard copy the file info to prevent modification of the original file info or the other way around
|
||||||
files := make([]*vfs.FileInfo, 0, len(m.files))
|
files := make([]*vfs.FileInfo, 0, len(m.files))
|
||||||
|
|||||||
@@ -1,63 +1,129 @@
|
|||||||
|
// vfs/memory/memory_test.go
|
||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
"s1d3sw1ped/SteamCache2/vfs/vfserror"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAllMemory(t *testing.T) {
|
func TestCreateAndOpen(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
m := New(1024)
|
m := New(1024)
|
||||||
if err := m.Set("key", []byte("value")); err != nil {
|
key := "key"
|
||||||
t.Errorf("Set failed: %v", err)
|
value := []byte("value")
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Set("key", []byte("value1")); err != nil {
|
w, err := m.Create(key, int64(len(value)))
|
||||||
t.Errorf("Set failed: %v", err)
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
}
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
if d, err := m.Get("key"); err != nil {
|
rc, err := m.Open(key)
|
||||||
t.Errorf("Get failed: %v", err)
|
if err != nil {
|
||||||
} else if string(d) != "value1" {
|
t.Fatalf("Open failed: %v", err)
|
||||||
t.Errorf("Get failed: got %s, want %s", d, "value1")
|
|
||||||
}
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
if err := m.Delete("key"); err != nil {
|
if string(got) != string(value) {
|
||||||
t.Errorf("Delete failed: %v", err)
|
t.Fatalf("expected %s, got %s", value, got)
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := m.Get("key"); err == nil {
|
|
||||||
t.Errorf("Get failed: got nil, want %v", vfserror.ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Delete("key"); err == nil {
|
|
||||||
t.Errorf("Delete failed: got nil, want %v", vfserror.ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := m.Stat("key"); err == nil {
|
|
||||||
t.Errorf("Stat failed: got nil, want %v", vfserror.ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Set("key", []byte("value")); err != nil {
|
|
||||||
t.Errorf("Set failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := m.Stat("key"); err != nil {
|
|
||||||
t.Errorf("Stat failed: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLimited(t *testing.T) {
|
func TestOverwrite(t *testing.T) {
|
||||||
t.Parallel()
|
m := New(1024)
|
||||||
|
key := "key"
|
||||||
|
value1 := []byte("value1")
|
||||||
|
value2 := []byte("value2")
|
||||||
|
|
||||||
|
w, err := m.Create(key, int64(len(value1)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(value1)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
w, err = m.Create(key, int64(len(value2)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(value2)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
rc, err := m.Open(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
got, _ := io.ReadAll(rc)
|
||||||
|
rc.Close()
|
||||||
|
|
||||||
|
if string(got) != string(value2) {
|
||||||
|
t.Fatalf("expected %s, got %s", value2, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
m := New(1024)
|
||||||
|
key := "key"
|
||||||
|
value := []byte("value")
|
||||||
|
|
||||||
|
w, err := m.Create(key, int64(len(value)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
if err := m.Delete(key); err != nil {
|
||||||
|
t.Fatalf("Delete failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = m.Open(key)
|
||||||
|
if !errors.Is(err, vfserror.ErrNotFound) {
|
||||||
|
t.Fatalf("expected %v, got %v", vfserror.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapacityLimit(t *testing.T) {
|
||||||
m := New(10)
|
m := New(10)
|
||||||
for i := 0; i < 11; i++ {
|
for i := 0; i < 11; i++ {
|
||||||
if err := m.Set(fmt.Sprintf("key%d", i), []byte("1")); err != nil && i < 10 {
|
w, err := m.Create(fmt.Sprintf("key%d", i), 1)
|
||||||
t.Errorf("Set failed: %v", err)
|
if err != nil && i < 10 {
|
||||||
|
t.Errorf("Create failed: %v", err)
|
||||||
} else if i == 10 && err == nil {
|
} else if i == 10 && err == nil {
|
||||||
t.Errorf("Set succeeded: got nil, want %v", vfserror.ErrDiskFull)
|
t.Errorf("Create succeeded: got nil, want %v", vfserror.ErrDiskFull)
|
||||||
|
}
|
||||||
|
if i < 10 {
|
||||||
|
w.Write([]byte("1"))
|
||||||
|
w.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStat(t *testing.T) {
|
||||||
|
m := New(1024)
|
||||||
|
key := "key"
|
||||||
|
value := []byte("value")
|
||||||
|
|
||||||
|
w, err := m.Create(key, int64(len(value)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Create failed: %v", err)
|
||||||
|
}
|
||||||
|
w.Write(value)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
info, err := m.Stat(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info == nil {
|
||||||
|
t.Fatal("expected file info to be non-nil")
|
||||||
|
}
|
||||||
|
if info.Size() != int64(len(value)) {
|
||||||
|
t.Errorf("expected size %d, got %d", len(value), info.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
package sync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"s1d3sw1ped/SteamCache2/vfs"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure SyncFS implements VFS.
|
|
||||||
var _ vfs.VFS = (*SyncFS)(nil)
|
|
||||||
|
|
||||||
type SyncFS struct {
|
|
||||||
vfs vfs.VFS
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(vfs vfs.VFS) *SyncFS {
|
|
||||||
return &SyncFS{
|
|
||||||
vfs: vfs,
|
|
||||||
mu: sync.RWMutex{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the file system.
|
|
||||||
func (sfs *SyncFS) Name() string {
|
|
||||||
return fmt.Sprintf("SyncFS(%s)", sfs.vfs.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the total size of all files in the file system.
|
|
||||||
func (sfs *SyncFS) Size() int64 {
|
|
||||||
sfs.mu.RLock()
|
|
||||||
defer sfs.mu.RUnlock()
|
|
||||||
|
|
||||||
return sfs.vfs.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets the value of key as src.
|
|
||||||
// Setting the same key multiple times, the last set call takes effect.
|
|
||||||
func (sfs *SyncFS) Set(key string, src []byte) error {
|
|
||||||
sfs.mu.Lock()
|
|
||||||
defer sfs.mu.Unlock()
|
|
||||||
|
|
||||||
return sfs.vfs.Set(key, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the value of key.
|
|
||||||
func (sfs *SyncFS) Delete(key string) error {
|
|
||||||
sfs.mu.Lock()
|
|
||||||
defer sfs.mu.Unlock()
|
|
||||||
|
|
||||||
return sfs.vfs.Delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get gets the value of key to dst, and returns dst no matter whether or not there is an error.
|
|
||||||
func (sfs *SyncFS) Get(key string) ([]byte, error) {
|
|
||||||
sfs.mu.RLock()
|
|
||||||
defer sfs.mu.RUnlock()
|
|
||||||
|
|
||||||
return sfs.vfs.Get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stat returns the FileInfo of key.
|
|
||||||
func (sfs *SyncFS) Stat(key string) (*vfs.FileInfo, error) {
|
|
||||||
sfs.mu.RLock()
|
|
||||||
defer sfs.mu.RUnlock()
|
|
||||||
|
|
||||||
return sfs.vfs.Stat(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatAll returns the FileInfo of all keys.
|
|
||||||
func (sfs *SyncFS) StatAll() []*vfs.FileInfo {
|
|
||||||
sfs.mu.RLock()
|
|
||||||
defer sfs.mu.RUnlock()
|
|
||||||
|
|
||||||
return sfs.vfs.StatAll()
|
|
||||||
}
|
|
||||||
12
vfs/vfs.go
12
vfs/vfs.go
@@ -1,5 +1,8 @@
|
|||||||
|
// vfs/vfs.go
|
||||||
package vfs
|
package vfs
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
// VFS is the interface that wraps the basic methods of a virtual file system.
|
// VFS is the interface that wraps the basic methods of a virtual file system.
|
||||||
type VFS interface {
|
type VFS interface {
|
||||||
// Name returns the name of the file system.
|
// Name returns the name of the file system.
|
||||||
@@ -8,15 +11,14 @@ type VFS interface {
|
|||||||
// Size returns the total size of all files in the file system.
|
// Size returns the total size of all files in the file system.
|
||||||
Size() int64
|
Size() int64
|
||||||
|
|
||||||
// Set sets the value of key as src.
|
// Create creates a new file at key with expected size.
|
||||||
// Setting the same key multiple times, the last set call takes effect.
|
Create(key string, size int64) (io.WriteCloser, error)
|
||||||
Set(key string, src []byte) error
|
|
||||||
|
|
||||||
// Delete deletes the value of key.
|
// Delete deletes the value of key.
|
||||||
Delete(key string) error
|
Delete(key string) error
|
||||||
|
|
||||||
// Get gets the value of key to dst, and returns dst no matter whether or not there is an error.
|
// Open opens the file at key.
|
||||||
Get(key string) ([]byte, error)
|
Open(key string) (io.ReadCloser, error)
|
||||||
|
|
||||||
// Stat returns the FileInfo of key.
|
// Stat returns the FileInfo of key.
|
||||||
Stat(key string) (*FileInfo, error)
|
Stat(key string) (*FileInfo, error)
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
// vfs/vfserror/vfserror.go
|
||||||
package vfserror
|
package vfserror
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
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 is returned when a code path is unreachable.
|
||||||
ErrUnreachable = errors.New("unreachable")
|
ErrUnreachable = errors.New("unreachable")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user