diff --git a/.github/change_version.sh b/.github/change_version.sh index b055985..3d037ac 100755 --- a/.github/change_version.sh +++ b/.github/change_version.sh @@ -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|CFBundleVersion\s*[^<]*|CFBundleVersion${VERSION_STR}|" Info.plist &&\ -SED -e "s|CFBundleShortVersionString\s*[^<]*|CFBundleShortVersionString${VERSION_STR}|" Info.plist &&\ +SED -e "s|CFBundleVersion\s*[^<]*|CFBundleVersion${VERSION_STR}|" Info.plist +SED -e "s|CFBundleShortVersionString\s*[^<]*|CFBundleShortVersionString${VERSION_STR}|" 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}" diff --git a/config/hiddify_option.go b/config/hiddify_option.go index 36fd367..636e78b 100644 --- a/config/hiddify_option.go +++ b/config/hiddify_option.go @@ -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"` diff --git a/config/warp.go b/config/warp.go index 81d2afe..76df0cb 100644 --- a/config/warp.go +++ b/config/warp.go @@ -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 { diff --git a/extension/extension.go b/extension/extension.go index fe6093a..c29c58a 100644 --- a/extension/extension.go +++ b/extension/extension.go @@ -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 { diff --git a/extension/extension_host.go b/extension/extension_host.go index 2585e65..6d8902c 100644 --- a/extension/extension_host.go +++ b/extension/extension_host.go @@ -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"` +} diff --git a/extension/html/rpc.js b/extension/html/rpc.js index 48d0a54..1679b1f 100644 --- a/extension/html/rpc.js +++ b/extension/html/rpc.js @@ -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); } diff --git a/extension/html/rpc/formRenderer.js b/extension/html/rpc/formRenderer.js index 646bd04..283876b 100644 --- a/extension/html/rpc/formRenderer.js +++ b/extension/html/rpc/formRenderer.js @@ -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); } diff --git a/extension/interface.go b/extension/interface.go index 1ba80f5..98839d3 100644 --- a/extension/interface.go +++ b/extension/interface.go @@ -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 diff --git a/go.mod b/go.mod index cfa395f..b41e784 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9583ea4..315cdb5 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/v2/common/cache.go b/v2/common/cache.go deleted file mode 100644 index 3edda6c..0000000 --- a/v2/common/cache.go +++ /dev/null @@ -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) - }) -} diff --git a/v2/db/db.go b/v2/db/db.go new file mode 100644 index 0000000..c6d94fa --- /dev/null +++ b/v2/db/db.go @@ -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 +}