mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-14 04:43:17 +00:00
219 lines
4.8 KiB
Go
219 lines
4.8 KiB
Go
package cachefile
|
|
|
|
import (
|
|
"errors"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/bbolt"
|
|
bboltErrors "github.com/sagernet/bbolt/errors"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing/common"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
)
|
|
|
|
var (
|
|
bucketSelected = []byte("selected")
|
|
bucketExpand = []byte("group_expand")
|
|
bucketMode = []byte("clash_mode")
|
|
|
|
bucketNameList = []string{
|
|
string(bucketSelected),
|
|
string(bucketExpand),
|
|
string(bucketMode),
|
|
}
|
|
|
|
cacheIDDefault = []byte("default")
|
|
)
|
|
|
|
var _ adapter.ClashCacheFile = (*CacheFile)(nil)
|
|
|
|
type CacheFile struct {
|
|
DB *bbolt.DB
|
|
cacheID []byte
|
|
saveAccess sync.RWMutex
|
|
saveDomain map[netip.Addr]string
|
|
saveAddress4 map[string]netip.Addr
|
|
saveAddress6 map[string]netip.Addr
|
|
saveMetadataTimer *time.Timer
|
|
}
|
|
|
|
func Open(path string, cacheID string) (*CacheFile, error) {
|
|
const fileMode = 0o666
|
|
options := bbolt.Options{Timeout: time.Second}
|
|
var (
|
|
db *bbolt.DB
|
|
err error
|
|
)
|
|
for i := 0; i < 10; i++ {
|
|
db, err = bbolt.Open(path, fileMode, &options)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if errors.Is(err, bboltErrors.ErrTimeout) {
|
|
continue
|
|
}
|
|
if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
|
|
rmErr := os.Remove(path)
|
|
if rmErr != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cacheIDBytes []byte
|
|
if cacheID != "" {
|
|
cacheIDBytes = append([]byte{0}, []byte(cacheID)...)
|
|
}
|
|
err = db.Batch(func(tx *bbolt.Tx) error {
|
|
return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
|
|
if name[0] == 0 {
|
|
return b.ForEachBucket(func(k []byte) error {
|
|
bucketName := string(k)
|
|
if !(common.Contains(bucketNameList, bucketName)) {
|
|
_ = b.DeleteBucket(name)
|
|
}
|
|
return nil
|
|
})
|
|
} else {
|
|
bucketName := string(name)
|
|
if !(common.Contains(bucketNameList, bucketName) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) {
|
|
_ = tx.DeleteBucket(name)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CacheFile{
|
|
DB: db,
|
|
cacheID: cacheIDBytes,
|
|
saveDomain: make(map[netip.Addr]string),
|
|
saveAddress4: make(map[string]netip.Addr),
|
|
saveAddress6: make(map[string]netip.Addr),
|
|
}, nil
|
|
}
|
|
|
|
func (c *CacheFile) LoadMode() string {
|
|
var mode string
|
|
c.DB.View(func(t *bbolt.Tx) error {
|
|
bucket := t.Bucket(bucketMode)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
var modeBytes []byte
|
|
if len(c.cacheID) > 0 {
|
|
modeBytes = bucket.Get(c.cacheID)
|
|
} else {
|
|
modeBytes = bucket.Get(cacheIDDefault)
|
|
}
|
|
mode = string(modeBytes)
|
|
return nil
|
|
})
|
|
return mode
|
|
}
|
|
|
|
func (c *CacheFile) StoreMode(mode string) error {
|
|
return c.DB.Batch(func(t *bbolt.Tx) error {
|
|
bucket, err := t.CreateBucketIfNotExists(bucketMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(c.cacheID) > 0 {
|
|
return bucket.Put(c.cacheID, []byte(mode))
|
|
} else {
|
|
return bucket.Put(cacheIDDefault, []byte(mode))
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket {
|
|
if c.cacheID == nil {
|
|
return t.Bucket(key)
|
|
}
|
|
bucket := t.Bucket(c.cacheID)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
return bucket.Bucket(key)
|
|
}
|
|
|
|
func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error) {
|
|
if c.cacheID == nil {
|
|
return t.CreateBucketIfNotExists(key)
|
|
}
|
|
bucket, err := t.CreateBucketIfNotExists(c.cacheID)
|
|
if bucket == nil {
|
|
return nil, err
|
|
}
|
|
return bucket.CreateBucketIfNotExists(key)
|
|
}
|
|
|
|
func (c *CacheFile) LoadSelected(group string) string {
|
|
var selected string
|
|
c.DB.View(func(t *bbolt.Tx) error {
|
|
bucket := c.bucket(t, bucketSelected)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
selectedBytes := bucket.Get([]byte(group))
|
|
if len(selectedBytes) > 0 {
|
|
selected = string(selectedBytes)
|
|
}
|
|
return nil
|
|
})
|
|
return selected
|
|
}
|
|
|
|
func (c *CacheFile) StoreSelected(group, selected string) error {
|
|
return c.DB.Batch(func(t *bbolt.Tx) error {
|
|
bucket, err := c.createBucket(t, bucketSelected)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return bucket.Put([]byte(group), []byte(selected))
|
|
})
|
|
}
|
|
|
|
func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
|
|
c.DB.View(func(t *bbolt.Tx) error {
|
|
bucket := c.bucket(t, bucketExpand)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
expandBytes := bucket.Get([]byte(group))
|
|
if len(expandBytes) == 1 {
|
|
isExpand = expandBytes[0] == 1
|
|
loaded = true
|
|
}
|
|
return nil
|
|
})
|
|
return
|
|
}
|
|
|
|
func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
|
|
return c.DB.Batch(func(t *bbolt.Tx) error {
|
|
bucket, err := c.createBucket(t, bucketExpand)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isExpand {
|
|
return bucket.Put([]byte(group), []byte{1})
|
|
} else {
|
|
return bucket.Put([]byte(group), []byte{0})
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *CacheFile) Close() error {
|
|
return c.DB.Close()
|
|
}
|