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
+}