package common import ( "context" "encoding/json" "errors" "log" "os" "time" "github.com/sagernet/sing-box/option" "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service/filemanager" ) var ( Storage = New(context.Background(), option.CacheFileOptions{}) bucketExtension = []byte("extension") bucketHiddify = []byte("hiddify") bucketNameList = []string{ string(bucketExtension), string(bucketHiddify), } ) type CacheFile struct { ctx context.Context path string cacheID []byte DB *bbolt.DB } func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { var path string if options.Path != "" { path = options.Path } else { path = "hiddify.db" } var cacheIDBytes []byte if options.CacheID != "" { cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) } cache := &CacheFile{ ctx: ctx, path: filemanager.BasePath(ctx, path), cacheID: cacheIDBytes, } err := cache.start() if err != nil { log.Panic(err) } return cache } func (c *CacheFile) start() 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(c.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(c.path) if rmErr != nil { return err } } time.Sleep(100 * time.Millisecond) } if err != nil { return err } err = filemanager.Chown(c.ctx, c.path) if err != nil { db.Close() return E.Cause(err, "platform chown") } 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)) { _ = tx.DeleteBucket(name) } } return nil }) }) if err != nil { db.Close() return err } c.DB = db return nil } 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) GetExtensionData(extension_id string, default_value any) error { err := c.DB.View(func(t *bbolt.Tx) error { bucket := c.bucket(t, bucketExtension) if bucket == nil { return os.ErrNotExist } setBinary := bucket.Get([]byte(extension_id)) if len(setBinary) == 0 { return os.ErrInvalid } return json.Unmarshal(setBinary, &default_value) }) return err } func (c *CacheFile) SaveExtensionData(extension_id string, data any) error { return c.DB.Batch(func(t *bbolt.Tx) error { bucket, err := c.createBucket(t, bucketExtension) if err != nil { return err } // Assuming T implements MarshalBinary setBinary, err := json.MarshalIndent(data, " ", "") if err != nil { return err } return bucket.Put([]byte(extension_id), setBinary) }) }