1
0
mirror of https://github.com/MetaCubeX/mihomo.git synced 2026-10-10 04:03:11 +08:00

feat: add /storage endpoint with Get, Put, and Delete methods for restful api

This commit is contained in:
Zephyruso
2026-04-19 20:45:12 +08:00
committed by GitHub
parent ec664a511f
commit 8a029da50b
4 changed files with 226 additions and 0 deletions
+1
View File
@@ -22,6 +22,7 @@ var (
bucketFakeip6 = []byte("fakeip6")
bucketETag = []byte("etag")
bucketSubscriptionInfo = []byte("subscriptioninfo")
bucketStorage = []byte("storage")
)
// CacheFile store and update the cache file
+165
View File
@@ -0,0 +1,165 @@
package cachefile
import (
"sort"
"time"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/bbolt"
"github.com/vmihailenco/msgpack/v5"
)
const storageSizeLimit = 1024 * 1024
const storageKeySizeLimit = 64
const maxStorageEntries = storageSizeLimit / storageKeySizeLimit
type StorageData struct {
Data []byte
Time time.Time
}
func decodeStorageData(v []byte) (StorageData, error) {
var storage StorageData
if err := msgpack.Unmarshal(v, &storage); err != nil {
return StorageData{}, err
}
return storage, nil
}
func (c *CacheFile) GetStorage(key string) []byte {
if c.DB == nil {
return nil
}
var data []byte
decodeFailed := false
err := c.DB.View(func(t *bbolt.Tx) error {
if bucket := t.Bucket(bucketStorage); bucket != nil {
if v := bucket.Get([]byte(key)); v != nil {
storage, err := decodeStorageData(v)
if err != nil {
decodeFailed = true
return err
}
data = storage.Data
}
}
return nil
})
if err != nil {
log.Warnln("[CacheFile] read cache for key %s failed: %s", key, err.Error())
if decodeFailed {
c.DeleteStorage(key)
}
return nil
}
return data
}
func (c *CacheFile) SetStorage(key string, data []byte) {
if c.DB == nil {
return
}
if len(key) > storageKeySizeLimit {
log.Warnln("[CacheFile] skip storage for key %s: key exceeds %d bytes", key, storageKeySizeLimit)
return
}
if len(data) > storageSizeLimit {
log.Warnln("[CacheFile] skip storage for key %s: payload exceeds %d bytes", key, storageSizeLimit)
return
}
keyBytes := []byte(key)
payload, err := msgpack.Marshal(StorageData{
Data: data,
Time: time.Now(),
})
if err != nil {
return
}
err = c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := t.CreateBucketIfNotExists(bucketStorage)
if err != nil {
return err
}
type storageEntry struct {
Key string
Data StorageData
}
entries := make(map[string]StorageData)
usedSize := 0
entryCount := 0
corruptedKeys := make([][]byte, 0)
c := bucket.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
storage, err := decodeStorageData(v)
if err != nil {
log.Warnln("[CacheFile] drop corrupted storage entry %s: %s", string(k), err.Error())
corruptedKeys = append(corruptedKeys, append([]byte(nil), k...))
continue
}
entryKey := string(k)
entries[entryKey] = storage
if entryKey != key {
usedSize += len(storage.Data)
entryCount++
}
}
for _, k := range corruptedKeys {
if err := bucket.Delete(k); err != nil {
return err
}
}
evictionQueue := make([]storageEntry, 0, len(entries))
for entryKey, storage := range entries {
if entryKey == key {
continue
}
evictionQueue = append(evictionQueue, storageEntry{
Key: entryKey,
Data: storage,
})
}
sort.Slice(evictionQueue, func(i, j int) bool {
left := evictionQueue[i]
right := evictionQueue[j]
if left.Data.Time.Equal(right.Data.Time) {
return left.Key < right.Key
}
return left.Data.Time.Before(right.Data.Time)
})
for _, entry := range evictionQueue {
if usedSize+len(data) <= storageSizeLimit && entryCount < maxStorageEntries {
break
}
if err := bucket.Delete([]byte(entry.Key)); err != nil {
return err
}
log.Infoln("[CacheFile] evict storage entry %s to make room for %s", entry.Key, key)
usedSize -= len(entry.Data.Data)
entryCount--
}
return bucket.Put(keyBytes, payload)
})
if err != nil {
log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error())
}
}
func (c *CacheFile) DeleteStorage(key string) {
if c.DB == nil {
return
}
err := c.DB.Batch(func(t *bbolt.Tx) error {
bucket := t.Bucket(bucketStorage)
if bucket == nil {
return nil
}
return bucket.Delete([]byte(key))
})
if err != nil {
log.Warnln("[CacheFile] delete cache from %s failed: %s", c.DB.Path(), err.Error())
}
}
+1
View File
@@ -132,6 +132,7 @@ func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux {
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/cache", cacheRouter())
r.Mount("/dns", dnsRouter())
r.Mount("/storage", storageRouter())
if !embedMode { // disallow restart in embed mode
r.Mount("/restart", restartRouter())
}
+59
View File
@@ -0,0 +1,59 @@
package route
import (
"encoding/json"
"io"
"github.com/metacubex/mihomo/component/profile/cachefile"
"github.com/metacubex/chi"
"github.com/metacubex/chi/render"
"github.com/metacubex/http"
)
func storageRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{key}", getStorage)
r.Put("/{key}", setStorage)
r.Delete("/{key}", deleteStorage)
return r
}
func getStorage(w http.ResponseWriter, r *http.Request) {
key := getEscapeParam(r, "key")
data := cachefile.Cache().GetStorage(key)
w.Header().Set("Content-Type", "application/json")
if len(data) == 0 {
w.Write([]byte("null"))
return
}
w.Write(data)
}
func setStorage(w http.ResponseWriter, r *http.Request) {
key := getEscapeParam(r, "key")
data, err := io.ReadAll(r.Body)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(err.Error()))
return
}
if !json.Valid(data) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
if len(data) > 1024*1024 {
render.Status(r, http.StatusRequestEntityTooLarge)
render.JSON(w, r, newError("payload exceeds 1MB limit"))
return
}
cachefile.Cache().SetStorage(key, data)
render.NoContent(w, r)
}
func deleteStorage(w http.ResponseWriter, r *http.Request) {
key := getEscapeParam(r, "key")
cachefile.Cache().DeleteStorage(key)
render.NoContent(w, r)
}