Skip to content

Commit a404d82

Browse files
committed
Wire up a simple explorer DB
1 parent 8ece863 commit a404d82

File tree

5 files changed

+227
-6
lines changed

5 files changed

+227
-6
lines changed

core/cli/explorer.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ package cli
22

33
import (
44
cliContext "github.com/mudler/LocalAI/core/cli/context"
5+
"github.com/mudler/LocalAI/core/explorer"
56
"github.com/mudler/LocalAI/core/http"
67
)
78

89
type ExplorerCMD struct {
9-
Address string `env:"LOCALAI_ADDRESS,ADDRESS" default:":8080" help:"Bind address for the API server" group:"api"`
10+
Address string `env:"LOCALAI_ADDRESS,ADDRESS" default:":8080" help:"Bind address for the API server" group:"api"`
11+
PoolDatabase string `env:"LOCALAI_POOL_DATABASE,POOL_DATABASE" default:"" help:"Path to the pool database" group:"api"`
1012
}
1113

12-
func (explorer *ExplorerCMD) Run(ctx *cliContext.Context) error {
13-
appHTTP := http.Explorer()
14+
func (e *ExplorerCMD) Run(ctx *cliContext.Context) error {
1415

15-
return appHTTP.Listen(explorer.Address)
16+
db, err := explorer.NewDatabase(e.PoolDatabase)
17+
if err != nil {
18+
return err
19+
}
20+
appHTTP := http.Explorer(db)
21+
22+
return appHTTP.Listen(e.Address)
1623
}

core/explorer/database.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package explorer
2+
3+
// A simple JSON database for storing and retrieving p2p network tokens and a name and description.
4+
5+
import (
6+
"encoding/json"
7+
"os"
8+
"sort"
9+
"sync"
10+
)
11+
12+
// Database is a simple JSON database for storing and retrieving p2p network tokens and a name and description.
13+
type Database struct {
14+
sync.RWMutex
15+
path string
16+
data map[string]TokenData
17+
}
18+
19+
// TokenData is a p2p network token with a name and description.
20+
type TokenData struct {
21+
Name string `json:"name"`
22+
Description string `json:"description"`
23+
}
24+
25+
// NewDatabase creates a new Database with the given path.
26+
func NewDatabase(path string) (*Database, error) {
27+
db := &Database{
28+
data: make(map[string]TokenData),
29+
path: path,
30+
}
31+
return db, db.load()
32+
}
33+
34+
// Get retrieves a Token from the Database by its token.
35+
func (db *Database) Get(token string) (TokenData, bool) {
36+
db.RLock()
37+
defer db.RUnlock()
38+
t, ok := db.data[token]
39+
return t, ok
40+
}
41+
42+
// Set stores a Token in the Database by its token.
43+
func (db *Database) Set(token string, t TokenData) error {
44+
db.Lock()
45+
db.data[token] = t
46+
db.Unlock()
47+
48+
return db.save()
49+
}
50+
51+
// Delete removes a Token from the Database by its token.
52+
func (db *Database) Delete(token string) error {
53+
db.Lock()
54+
delete(db.data, token)
55+
db.Unlock()
56+
return db.save()
57+
}
58+
59+
func (db *Database) TokenList() []string {
60+
db.RLock()
61+
defer db.RUnlock()
62+
tokens := []string{}
63+
for k := range db.data {
64+
tokens = append(tokens, k)
65+
}
66+
67+
sort.Slice(tokens, func(i, j int) bool {
68+
// sort by token
69+
return tokens[i] < tokens[j]
70+
})
71+
72+
return tokens
73+
}
74+
75+
// load reads the Database from disk.
76+
func (db *Database) load() error {
77+
db.Lock()
78+
defer db.Unlock()
79+
80+
if _, err := os.Stat(db.path); os.IsNotExist(err) {
81+
82+
return nil
83+
}
84+
85+
// Read the file from disk
86+
// Unmarshal the JSON into db.data
87+
f, err := os.ReadFile(db.path)
88+
if err != nil {
89+
return err
90+
}
91+
return json.Unmarshal(f, &db.data)
92+
}
93+
94+
// save writes the Database to disk.
95+
func (db *Database) save() error {
96+
db.RLock()
97+
defer db.RUnlock()
98+
99+
// Marshal db.data into JSON
100+
// Write the JSON to the file
101+
f, err := os.Create(db.path)
102+
if err != nil {
103+
return err
104+
}
105+
defer f.Close()
106+
return json.NewEncoder(f).Encode(db.data)
107+
}

core/explorer/database_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package explorer_test
2+
3+
import (
4+
"os"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
9+
"github.com/mudler/LocalAI/core/explorer"
10+
)
11+
12+
var _ = Describe("Database", func() {
13+
var (
14+
dbPath string
15+
db *explorer.Database
16+
err error
17+
)
18+
19+
BeforeEach(func() {
20+
// Create a temporary file path for the database
21+
dbPath = "test_db.json"
22+
db, err = explorer.NewDatabase(dbPath)
23+
Expect(err).To(BeNil())
24+
})
25+
26+
AfterEach(func() {
27+
// Clean up the temporary database file
28+
os.Remove(dbPath)
29+
})
30+
31+
Context("when managing tokens", func() {
32+
It("should add and retrieve a token", func() {
33+
token := "token123"
34+
t := explorer.TokenData{Name: "TokenName", Description: "A test token"}
35+
36+
err = db.Set(token, t)
37+
Expect(err).To(BeNil())
38+
39+
retrievedToken, exists := db.Get(token)
40+
Expect(exists).To(BeTrue())
41+
Expect(retrievedToken).To(Equal(t))
42+
})
43+
44+
It("should delete a token", func() {
45+
token := "token123"
46+
t := explorer.TokenData{Name: "TokenName", Description: "A test token"}
47+
48+
err = db.Set(token, t)
49+
Expect(err).To(BeNil())
50+
51+
err = db.Delete(token)
52+
Expect(err).To(BeNil())
53+
54+
_, exists := db.Get(token)
55+
Expect(exists).To(BeFalse())
56+
})
57+
58+
It("should persist data to disk", func() {
59+
token := "token123"
60+
t := explorer.TokenData{Name: "TokenName", Description: "A test token"}
61+
62+
err = db.Set(token, t)
63+
Expect(err).To(BeNil())
64+
65+
// Recreate the database object to simulate reloading from disk
66+
db, err = explorer.NewDatabase(dbPath)
67+
Expect(err).To(BeNil())
68+
69+
retrievedToken, exists := db.Get(token)
70+
Expect(exists).To(BeTrue())
71+
Expect(retrievedToken).To(Equal(t))
72+
73+
// Check the token list
74+
tokenList := db.TokenList()
75+
Expect(tokenList).To(ContainElement(token))
76+
})
77+
})
78+
79+
Context("when loading an empty or non-existent file", func() {
80+
It("should start with an empty database", func() {
81+
dbPath = "empty_db.json"
82+
db, err = explorer.NewDatabase(dbPath)
83+
Expect(err).To(BeNil())
84+
85+
_, exists := db.Get("nonexistent")
86+
Expect(exists).To(BeFalse())
87+
88+
// Clean up
89+
os.Remove(dbPath)
90+
})
91+
})
92+
})

core/explorer/explorer_suite_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package explorer_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestExplorer(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Explorer test suite")
13+
}

core/http/explorer.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ package http
22

33
import (
44
"github.com/gofiber/fiber/v2"
5+
"github.com/mudler/LocalAI/core/explorer"
56
"github.com/mudler/LocalAI/core/http/routes"
67
)
78

8-
func Explorer() *fiber.App {
9+
func Explorer(db *explorer.Database) *fiber.App {
910

1011
fiberCfg := fiber.Config{
1112
Views: renderEngine(),
1213
// We disable the Fiber startup message as it does not conform to structured logging.
1314
// We register a startup log line with connection information in the OnListen hook to keep things user friendly though
14-
DisableStartupMessage: true,
15+
DisableStartupMessage: false,
1516
// Override default error handler
1617
}
1718

1819
app := fiber.New(fiberCfg)
20+
1921
routes.RegisterExplorerRoutes(app)
2022

2123
return app

0 commit comments

Comments
 (0)