new: add multiprocess safe database

This commit is contained in:
hiddify
2024-09-30 21:10:33 +02:00
parent e8e7efc513
commit 51862c6e99
12 changed files with 361 additions and 352 deletions

View File

@@ -5,14 +5,14 @@ SED() { [[ "$OSTYPE" == "darwin"* ]] && sed -i '' "$@" || sed -i "$@"; }
echo "previous version was $(git describe --tags $(git rev-list --tags --max-count=1))"
echo "WARNING: This operation will creates version tag and push to github"
read -p "Version? (provide the next x.y.z semver) : " TAG
echo $TAG &&\
echo $TAG
[[ "$TAG" =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(\.dev)?$ ]] || { echo "Incorrect tag. e.g., 1.2.3 or 1.2.3.dev"; exit 1; }
IFS="." read -r -a VERSION_ARRAY <<< "$TAG"
VERSION_STR="${VERSION_ARRAY[0]}.${VERSION_ARRAY[1]}.${VERSION_ARRAY[2]}"
BUILD_NUMBER=$(( ${VERSION_ARRAY[0]} * 10000 + ${VERSION_ARRAY[1]} * 100 + ${VERSION_ARRAY[2]} ))
echo "version: ${VERSION_STR}+${BUILD_NUMBER}"
SED -e "s|<key>CFBundleVersion</key>\s*<string>[^<]*</string>|<key>CFBundleVersion</key><string>${VERSION_STR}</string>|" Info.plist &&\
SED -e "s|<key>CFBundleShortVersionString</key>\s*<string>[^<]*</string>|<key>CFBundleShortVersionString</key><string>${VERSION_STR}</string>|" Info.plist &&\
SED -e "s|<key>CFBundleVersion</key>\s*<string>[^<]*</string>|<key>CFBundleVersion</key><string>${VERSION_STR}</string>|" Info.plist
SED -e "s|<key>CFBundleShortVersionString</key>\s*<string>[^<]*</string>|<key>CFBundleShortVersionString</key><string>${VERSION_STR}</string>|" Info.plist
SED "s|ENV VERSION=.*|ENV VERSION=v${TAG}|g" docker/Dockerfile
git add Info.plist docker/Dockerfile
git commit -m "release: version ${TAG}"

View File

@@ -80,6 +80,7 @@ type MuxOptions struct {
}
type WarpOptions struct {
UniqueId string `json:"unique-id"`
EnableWarp bool `json:"enable"`
Mode string `json:"mode"`
WireguardConfigStr string `json:"wireguard-config"`

View File

@@ -13,6 +13,7 @@ import (
C "github.com/sagernet/sing-box/constant"
// "github.com/bepass-org/wireguard-go/warp"
"github.com/hiddify/hiddify-core/v2/db"
"github.com/sagernet/sing-box/option"
T "github.com/sagernet/sing-box/option"
@@ -143,34 +144,31 @@ func GenerateWarpInfo(license string, oldAccountId string, oldAccessToken string
return &identity, res, &warpcfg, err
}
func getOrGenerateWarpLocallyIfNeeded(key string, warpOptions *WarpOptions) WarpWireguardConfig {
if warpOptions == nil {
warpOptions = &WarpOptions{}
}
func getOrGenerateWarpLocallyIfNeeded(warpOptions *WarpOptions) WarpWireguardConfig {
if warpOptions.WireguardConfig.PrivateKey != "" {
return warpOptions.WireguardConfig
}
common.Storage.GetExtensionData("hiddify.warp."+key, &warpOptions)
if warpOptions.WireguardConfig.PrivateKey != "" {
table := db.GetTable[WarpOptions]()
dbWarpOptions, err := table.First(func(data WarpOptions) bool { return data.UniqueId == warpOptions.UniqueId })
if err == nil && dbWarpOptions.WireguardConfig.PrivateKey != "" {
return warpOptions.WireguardConfig
}
license := ""
if len(key) > 28 && key[2] == '_' { // warp key is 26 characters long
license = key[3:]
if len(warpOptions.UniqueId) > 28 && warpOptions.UniqueId[2] == '_' { // warp key is 26 characters long
license = warpOptions.UniqueId[3:]
}
accountidentity, _, wireguardConfig, err := GenerateWarpInfo(license, warpOptions.Account.AccountID, warpOptions.Account.AccessToken)
if err != nil {
return WarpWireguardConfig{}
}
newoption := WarpOptions{
WireguardConfig: *wireguardConfig,
Account: WarpAccount{
AccountID: accountidentity.ID,
AccessToken: accountidentity.Token,
},
warpOptions.Account = WarpAccount{
AccountID: accountidentity.ID,
AccessToken: accountidentity.Token,
}
common.Storage.SaveExtensionData("hiddify.warp."+key, &newoption)
return newoption.WireguardConfig
warpOptions.WireguardConfig = *wireguardConfig
table.ReplaceOrInsert(func(data WarpOptions) bool { return data.UniqueId == warpOptions.UniqueId }, *warpOptions)
return *wireguardConfig
}
func patchWarp(base *option.Outbound, configOpt *HiddifyOptions, final bool, staticIpsDns map[string][]string) error {
@@ -199,8 +197,13 @@ func patchWarp(base *option.Outbound, configOpt *HiddifyOptions, final bool, sta
warpOpt = &configOpt.Warp
} else if key == "p2" {
warpOpt = &configOpt.Warp2
} else {
warpOpt = &WarpOptions{
UniqueId: key,
}
}
wireguardConfig = getOrGenerateWarpLocallyIfNeeded(key, warpOpt)
warpOpt.UniqueId = key
wireguardConfig = getOrGenerateWarpLocallyIfNeeded(warpOpt)
} else {
_, _, wgConfig, err := GenerateWarpInfo(key, "", "")
if err != nil {

View File

@@ -1,10 +1,12 @@
package extension
import (
"reflect"
"github.com/hiddify/hiddify-core/config"
"github.com/hiddify/hiddify-core/extension/ui"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/hiddify/hiddify-core/v2/common"
"github.com/hiddify/hiddify-core/v2/db"
"github.com/jellydator/validation"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
@@ -12,7 +14,7 @@ import (
type Extension interface {
GetUI() ui.Form
SubmitData(data map[string]string) error
SubmitData(button string, data map[string]string) error
Cancel() error
Stop() error
UpdateUI(form ui.Form) error
@@ -41,13 +43,37 @@ func (b *Base[T]) BeforeAppConnect(hiddifySettings *config.HiddifyOptions, singc
}
func (b *Base[T]) StoreData() {
common.Storage.SaveExtensionData(b.id, &b.Data)
table := db.GetTable[extensionData]()
table.Update(func(s extensionData) extensionData {
s.Data = b.Data
return s
}, func(data extensionData) bool {
return data.Id == b.getId()
})
}
func (b *Base[T]) init(id string) {
b.id = id
b.queue = make(chan *pb.ExtensionResponse, 1)
common.Storage.GetExtensionData(b.id, &b.Data)
table := db.GetTable[extensionData]()
extdata, err := table.First(func(data extensionData) bool { return data.Id == b.id })
if err != nil {
log.Warn("error: ", err)
return
}
if extdata == nil {
log.Warn("extension data not found ", id)
return
}
if extdata.Data != nil {
if data, ok := extdata.Data.(*T); ok {
b.Data = *data
} else {
var t T
name := reflect.TypeOf(t).Name()
log.Warn("current extension data of ,", id, " is not of type ", name)
}
}
}
func (b *Base[T]) getQueue() chan *pb.ExtensionResponse {

View File

@@ -6,7 +6,7 @@ import (
"log"
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
"github.com/hiddify/hiddify-core/v2/common"
"github.com/hiddify/hiddify-core/v2/db"
"google.golang.org/grpc"
)
@@ -18,141 +18,71 @@ func (ExtensionHostService) ListExtensions(ctx context.Context, empty *pb.Empty)
extensionList := &pb.ExtensionList{
Extensions: make([]*pb.Extension, 0),
}
for _, extension := range allExtensionsMap {
allext, err := db.GetTable[extensionData]().All()
if err != nil {
return nil, err
}
for _, dbext := range allext {
ext := allExtensionsMap[dbext.Id]
extensionList.Extensions = append(extensionList.Extensions, &pb.Extension{
Id: extension.Id,
Title: extension.Title,
Description: extension.Description,
Enable: generalExtensionData.ExtensionStatusMap[extension.Id],
Id: ext.Id,
Title: ext.Title,
Description: ext.Description,
Enable: dbext.Enable,
})
}
return extensionList, nil
}
func getExtension(id string) (*Extension, error) {
if !isEnable(id) {
return nil, fmt.Errorf("Extension with ID %s is not enabled", id)
}
if extension, ok := enabledExtensionsMap[id]; ok {
return extension, nil
}
return nil, fmt.Errorf("Extension with ID %s not found", id)
}
func (e ExtensionHostService) Connect(req *pb.ExtensionRequest, stream grpc.ServerStreamingServer[pb.ExtensionResponse]) error {
// Get the extension from the map using the Extension ID
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
extension, err := getExtension(req.GetExtensionId())
if err != nil {
log.Printf("Error connecting stream for extension %s: %v", req.GetExtensionId(), err)
return err
}
log.Printf("Connecting stream for extension %s", req.GetExtensionId())
log.Printf("Extension data: %+v", extension)
// Handle loading the UI for the extension
// Call extension-specific logic to generate UI data
// if err := platform.connect(stream); err != nil {
// log.Printf("Error connecting stream for extension %s: %v", req.GetExtensionId(), err)
// }
if err := (*extension).UpdateUI((*extension).GetUI()); err != nil {
log.Printf("Error updating UI for extension %s: %v", req.GetExtensionId(), err)
}
// info := <-platform.queue
log.Printf("Connecting stream for extension %s", req.GetExtensionId())
log.Printf("Extension data: %+v", extension)
// stream.Send(info)
// (*platform.extension).SubmitData(map[string]string{})
// log.Printf("Extension info: %+v", info)
// // Handle submitting data to the extension
// case pb.ExtensionRequestType_SUBMIT_DATA:
// // Handle submitting data to the extension
// // Process the provided data
// err := extension.SubmitData(req.GetData())
// if err != nil {
// log.Printf("Error submitting data for extension %s: %v", req.GetExtensionId(), err)
// // continue
// }
if err := (*extension).UpdateUI((*extension).GetUI()); err != nil {
log.Printf("Error updating UI for extension %s: %v", req.GetExtensionId(), err)
}
// case hiddifyrpc.ExtensionRequestType_CANCEL:
// // Handle canceling the current operation in the extension
// extension.Stop()
// log.Printf("Operation canceled for extension %s", req.GetExtensionId())
// default:
// log.Printf("Unknown request type: %v", req.GetType())
// }
for {
select {
case <-stream.Context().Done():
for {
select {
case <-stream.Context().Done():
return nil
case info := <-(*extension).getQueue():
stream.Send(info)
if info.GetType() == pb.ExtensionResponseType_END {
return nil
case info := <-(*extension).getQueue():
stream.Send(info)
if info.GetType() == pb.ExtensionResponseType_END {
return nil
}
}
}
// break
// case <-stopCh:
// break
// // case info := <-sub:
// // stream.Send(&info)
// case <-time.After(1000 * time.Millisecond):
// }
// extension := extensionsMap[data.GetExtensionId()]
// ui := extension.GetUI(data.Data)
// return &pb.UI{
// ExtensionId: data.GetExtensionId(),
// JsonUi: ui.ToJSON(),
// }, nil
} else {
log.Printf("Extension with ID %s not found", req.GetExtensionId())
return fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
}
}
func (e ExtensionHostService) SubmitForm(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
(*extension).SubmitData(req.GetData())
extension, err := getExtension(req.GetExtensionId())
if err != nil {
log.Println(err)
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_OK,
Message: "Success",
}, nil
Code: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
}
func (e ExtensionHostService) Cancel(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
(*extension).Cancel()
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_OK,
Message: "Success",
}, nil
}
return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
}
func (e ExtensionHostService) Stop(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
(*extension).Stop()
(*extension).StoreData()
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_OK,
Message: "Success",
}, nil
}
return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
}
func (e ExtensionHostService) EditExtension(ctx context.Context, req *pb.EditExtensionRequest) (*pb.ExtensionActionResult, error) {
generalExtensionData.ExtensionStatusMap[req.GetExtensionId()] = req.Enable
if !req.Enable {
ext := *enabledExtensionsMap[req.GetExtensionId()]
if ext != nil {
ext.Stop()
ext.StoreData()
}
delete(enabledExtensionsMap, req.GetExtensionId())
} else {
loadExtension(allExtensionsMap[req.GetExtensionId()])
}
common.Storage.SaveExtensionData("default", generalExtensionData)
(*extension).SubmitData(req.GetData())
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
@@ -160,3 +90,75 @@ func (e ExtensionHostService) EditExtension(ctx context.Context, req *pb.EditExt
Message: "Success",
}, nil
}
func (e ExtensionHostService) Cancel(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
extension, err := getExtension(req.GetExtensionId())
if err != nil {
log.Println(err)
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
(*extension).Cancel()
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_OK,
Message: "Success",
}, nil
}
func (e ExtensionHostService) Stop(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
extension, err := getExtension(req.GetExtensionId())
if err != nil {
log.Println(err)
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_FAILED,
Message: err.Error(),
}, err
}
(*extension).Stop()
(*extension).StoreData()
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_OK,
Message: "Success",
}, nil
}
func (e ExtensionHostService) EditExtension(ctx context.Context, req *pb.EditExtensionRequest) (*pb.ExtensionActionResult, error) {
if !req.Enable {
extension, _ := getExtension(req.GetExtensionId())
if extension != nil {
(*extension).Stop()
(*extension).StoreData()
}
delete(enabledExtensionsMap, req.GetExtensionId())
}
table := db.GetTable[extensionData]()
table.Update(func(s extensionData) extensionData {
s.Enable = req.Enable
return s
}, func(data extensionData) bool {
return data.Id == req.GetExtensionId()
})
if req.Enable {
loadExtension(allExtensionsMap[req.GetExtensionId()])
}
return &pb.ExtensionActionResult{
ExtensionId: req.ExtensionId,
Code: pb.ResponseCode_OK,
Message: "Success",
}, nil
}
type extensionData struct {
Id string `json:"id"`
Enable bool `json:"enable"`
Data any `json:"data"`
}

View File

@@ -708,7 +708,7 @@ function connect() {
if(response.getType()== proto.hiddifyrpc.ExtensionResponseType.SHOW_DIALOG) {
renderForm(ui, "dialog",handleSubmitButtonClick,handleCancelButtonClick,undefined);
}else{
renderForm(ui, "",handleSubmitButtonClick,handleCancelButtonClick);
renderForm(ui, "",handleSubmitButtonClick,handleCancelButtonClick,handleStopButtonClick);
}
@@ -2576,11 +2576,16 @@ function renderForm(json, dialog, submitAction, cancelAction, stopAction) {
if (dialog === "dialog") {
document.getElementById("modal-footer").innerHTML = '';
document.getElementById("modal-footer").appendChild(buttonGroup);
const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
dialog.show()
dialog.on("hidden.bs.modal", () => {
cancelAction()
})
const extensionDialog = document.getElementById("extension-dialog");
const dialog = bootstrap.Modal.getOrCreateInstance(extensionDialog);
dialog.show();
extensionDialog.addEventListener("hidden.bs.modal", cancelAction);
// const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
// dialog.show()
// dialog.on("hidden.bs.modal", () => {
// cancelAction()
// })
} else {
form.appendChild(buttonGroup);
}

View File

@@ -35,11 +35,16 @@ function renderForm(json, dialog, submitAction, cancelAction, stopAction) {
if (dialog === "dialog") {
document.getElementById("modal-footer").innerHTML = '';
document.getElementById("modal-footer").appendChild(buttonGroup);
const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
dialog.show()
dialog.on("hidden.bs.modal", () => {
cancelAction()
})
const extensionDialog = document.getElementById("extension-dialog");
const dialog = bootstrap.Modal.getOrCreateInstance(extensionDialog);
dialog.show();
extensionDialog.addEventListener("hidden.bs.modal", cancelAction);
// const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
// dialog.show()
// dialog.on("hidden.bs.modal", () => {
// cancelAction()
// })
} else {
form.appendChild(buttonGroup);
}

View File

@@ -4,7 +4,8 @@ import (
"fmt"
"log"
"github.com/hiddify/hiddify-core/v2/common"
"github.com/hiddify/hiddify-core/v2/db"
"github.com/hiddify/hiddify-core/v2/service_manager"
)
@@ -26,12 +27,29 @@ func RegisterExtension(factory ExtensionFactory) error {
log.Fatal(err)
return err
}
table := db.GetTable[extensionData]()
_, err := table.FirstOrInsert(func(data extensionData) bool { return data.Id == factory.Id }, func() extensionData { return extensionData{Id: factory.Id, Enable: false} })
if err != nil {
return err
}
allExtensionsMap[factory.Id] = factory
return nil
}
func isEnable(id string) bool {
table := db.GetTable[extensionData]()
extdata, err := table.First(func(data extensionData) bool { return data.Id == id })
if err != nil {
return false
}
return extdata.Enable
}
func loadExtension(factory ExtensionFactory) error {
if !isEnable(factory.Id) {
return fmt.Errorf("Extension with ID %s is not enabled", factory.Id)
}
extension := factory.Builder()
extension.init(factory.Id)
@@ -46,11 +64,18 @@ type extensionService struct {
}
func (s *extensionService) Start() error {
common.Storage.GetExtensionData("default", &generalExtensionData)
for id, factory := range allExtensionsMap {
if val, ok := generalExtensionData.ExtensionStatusMap[id]; ok && val {
loadExtension(factory)
table := db.GetTable[extensionData]()
extdata, err := table.Select(func(data extensionData) bool { return data.Enable })
if err != nil {
return fmt.Errorf("failed to select enabled extensions: %w", err)
}
for _, data := range extdata {
if factory, ok := allExtensionsMap[data.Id]; ok {
if err := loadExtension(factory); err != nil {
return fmt.Errorf("failed to load extension %s: %w", data.Id, err)
}
} else {
return fmt.Errorf("extension %s is enabled but not found", data.Id)
}
}
return nil

5
go.mod
View File

@@ -23,7 +23,10 @@ require (
gopkg.in/yaml.v3 v3.0.1
)
require github.com/hiddify/hiddify-app-demo-extension v0.0.0-20240929132536-e158b83e958c
require (
github.com/Yiwen-Chan/tinydb v0.0.0-20230129042445-3321642f0674
github.com/hiddify/hiddify-app-demo-extension v0.0.0-20240929132536-e158b83e958c
)
require (
github.com/cenkalti/backoff/v4 v4.1.1 // indirect

2
go.sum
View File

@@ -18,6 +18,8 @@ github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGav
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/Yiwen-Chan/tinydb v0.0.0-20230129042445-3321642f0674 h1:Sf029Pn6NCxD0TP/AeEO87epoaNeCtUFrCHKndEc3G0=
github.com/Yiwen-Chan/tinydb v0.0.0-20230129042445-3321642f0674/go.mod h1:FKpvt4bXlMiJn5DipBosCuM1tH27p0z9RI3sHRMH+40=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=

View File

@@ -1,194 +0,0 @@
package common
import (
"context"
"encoding/json"
"errors"
"log"
"os"
"time"
"github.com/hiddify/hiddify-core/v2/service_manager"
"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 CacheFile
bucketExtension = []byte("extension")
bucketHiddify = []byte("hiddify")
bucketNameList = []string{
string(bucketExtension),
string(bucketHiddify),
}
)
type StorageService struct {
// Storage *CacheFile
}
func (s *StorageService) Start() error {
Storage = *NewStorage(context.Background(), option.CacheFileOptions{})
return nil
}
func (s *StorageService) Close() error {
if Storage.DB != nil {
Storage.DB.Close()
}
return nil
}
func init() {
service_manager.RegisterPreservice(&StorageService{})
}
type CacheFile struct {
ctx context.Context
path string
cacheID []byte
DB *bbolt.DB
}
func NewStorage(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)
})
}

131
v2/db/db.go Normal file
View File

@@ -0,0 +1,131 @@
package db
import (
"fmt"
"os"
"reflect"
tinydb "github.com/Yiwen-Chan/tinydb"
)
type DB struct {
tdb *tinydb.Database
}
var instance map[string]*DB = make(map[string]*DB)
func Instance(name string) *DB {
if db, ok := instance[name]; ok {
return db
}
os.MkdirAll("data", 0o700)
db, err := NewDB("data/hiddify-db-" + name + ".json")
if err != nil {
fmt.Println("Default DB instance failed", err)
}
instance[name] = db
return db
}
func NewDB(path string) (*DB, error) {
storage, err := tinydb.JSONStorage(path)
if err != nil {
return nil, err
}
tdb, err := tinydb.TinyDB(storage)
if err != nil {
return nil, err
}
return &DB{
tdb: tdb,
}, nil
}
func (d *DB) Close() error {
return d.tdb.Close()
}
func GetTableDB[T any](db *DB) *Table[T] {
tt := tinydb.GetTable[T](db.tdb)
if tt == nil {
return nil
}
return &Table[T]{
Table: tt,
}
}
func GetTable[T any]() *Table[T] {
var t T
name := reflect.TypeOf(t).Name()
tt := tinydb.GetTable[T](Instance(name).tdb)
if tt == nil {
return nil
}
return &Table[T]{
Table: tt,
}
}
type Table[T any] struct {
*tinydb.Table[T]
}
func (tbl *Table[T]) Select(selector func(T) bool) ([]T, error) {
return tbl.Table.Select(selector)
}
func (tbl *Table[T]) All() ([]T, error) {
return tbl.Table.Select(func(T) bool {
return true
})
}
func (tbl *Table[T]) Insert(items ...T) error {
return tbl.Table.Insert(items...)
}
func (tbl *Table[T]) Delete(selector func(T) bool) ([]T, error) {
return tbl.Table.Delete(selector)
}
func (tbl *Table[T]) Update(update func(T) T, selector func(T) bool) error {
return tbl.Table.Update(update, selector)
}
func (tbl *Table[T]) First(selector func(T) bool) (*T, error) {
data, err := tbl.Select(selector)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("not found")
}
return &data[0], nil
}
func (table *Table[T]) FirstOrInsert(selector func(d T) bool, generator func() T) (*T, error) {
data, err := table.First(selector)
if err == nil {
return data, nil
}
if err := table.Insert(generator()); err != nil {
return nil, err
}
return table.First(selector)
}
func (table *Table[T]) ReplaceOrInsert(selector func(d T) bool, generator T) error {
data, err := table.First(selector)
if err == nil && data != nil {
if _, err := table.Delete(selector); err != nil {
return err
}
}
if err := table.Insert(generator); err != nil {
return err
}
return nil
}