tooling away on data persistent stuff

This commit is contained in:
kayos@tcp.direct 2022-12-16 12:53:46 -08:00
parent d60c95b2c6
commit 2af9e5835b
Signed by: kayos
GPG Key ID: 4B841471B4BEE979
8 changed files with 297 additions and 117 deletions

View File

@ -1,23 +0,0 @@
package data
import (
"strings"
"git.tcp.direct/tcp.direct/database"
)
func kva() database.Store {
return kv().With("aliases")
}
func AddAlias(alias, command string) error {
return kva().Put([]byte(strings.ToLower(strings.TrimSpace(alias))), []byte(command))
}
func GetAlias(alias, command string) (cmd string) {
a, err := kva().Get([]byte(strings.ToLower(strings.TrimSpace(alias))))
if err == nil {
cmd = string(a)
}
return
}

View File

@ -13,7 +13,7 @@ import (
)
var (
stores = []string{"aliases", "sequences"}
stores = []string{"macros"}
istest = false
once = &sync.Once{}
target string

View File

@ -0,0 +1,30 @@
package data
// WIP concepts:
/*type ArgType uint8
const (
ArgTypeString ArgType = iota
ArgTypeInt
ArgTypeFloat
ArgTypeBool
ArgTypeColor // constrained to hex and color alias strings
ArgTypeSaturation // constrained to 1-255
ArgTypeDuration // constrained to time.Duration parsable format
ArgTypeTime // constrained to time.Time parsable format
ArgTypeDate // constrained to time.Time parsable format
)
// AddFunction example:
//
// desired: ziggs> test green
//
// AddFunction("test", map[int]data.ArgType{
// 0: "",
// })
func AddFunction(function string, args map[int]string) error {
}
*/

106
internal/data/macros.go Normal file
View File

@ -0,0 +1,106 @@
package data
import (
"encoding/json"
"fmt"
"strings"
"sync"
"git.tcp.direct/kayos/common/squish"
"git.tcp.direct/tcp.direct/database"
)
func kvMacros() database.Store {
return kv().With("macros")
}
type Macro struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Sequence []string `json:"sequence"`
}
type macroCache struct {
cache map[string]*Macro
*sync.RWMutex
}
var macros = &macroCache{
cache: make(map[string]*Macro),
RWMutex: new(sync.RWMutex),
}
func hasMacroCached(name string) *Macro {
macros.RLock()
defer macros.RUnlock()
if macroPtr, ok := macros.cache[name]; ok {
return macroPtr
}
return nil
}
func unpackMacro(input []byte) (macro *Macro, err error) {
if input, err = squish.Gunzip(input); err != nil {
return nil, fmt.Errorf("error deflating macro: %w", err)
}
err = json.Unmarshal(input, &macro)
return
}
func updateCache(name string, macro *Macro) {
macros.Lock()
macros.cache[name] = macro
macros.Unlock()
}
func GetMacro(name string) (mcro *Macro, err error) {
name = strings.ToLower(strings.TrimSpace(name))
if cached := hasMacroCached(name); cached != nil {
return cached, nil
}
if !kvMacros().Has([]byte(name)) {
}
var packed []byte
if packed, err = kvMacros().Get([]byte(name)); err != nil {
return nil, fmt.Errorf("error fetching macro: %w", err)
}
if mcro, err = unpackMacro(packed); err != nil {
return nil, fmt.Errorf("error unpacking macro: %w", err)
}
go updateCache(name, mcro)
return mcro, err
}
func DeleteMacro(name string) error {
macros.Lock()
defer macros.Unlock()
delete(macros.cache, name)
return fmt.Errorf(
"failed to delete macro: %w",
kvMacros().Delete([]byte(strings.ToLower(strings.TrimSpace(name)))),
)
}
// AddMacro adds a macro to the database, the description is optional.
func AddMacro(name string, description string, sequence ...string) error {
if _, err := GetMacro(name); err == nil {
return fmt.Errorf("a macro named %q already exists", name)
}
mcro := Macro{
Name: name,
Description: description,
Sequence: sequence,
}
rawMacro, err := json.Marshal(mcro)
if err != nil {
return fmt.Errorf("failed to marshal macro: %w", err)
}
rawMacro = squish.Gzip(rawMacro)
return kvMacros().Put([]byte(strings.ToLower(strings.TrimSpace(name))), rawMacro)
}

View File

@ -0,0 +1,98 @@
package squish
import (
"bytes"
"compress/gzip"
"encoding/base64"
"errors"
"io"
"sync"
)
var (
bufPool = &sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
gzipPool = &sync.Pool{
New: func() interface{} {
return gzip.NewWriter(nil)
},
}
)
// Gzip compresses as slice of bytes using gzip compression.
func Gzip(data []byte) []byte {
buf := bufPool.Get().(*bytes.Buffer)
gz := gzipPool.Get().(*gzip.Writer)
buf.Reset()
r, w := io.Pipe()
gz.Reset(w)
go func() {
_, _ = gz.Write(data)
_ = gz.Close()
_ = w.Close()
}()
n, _ := buf.ReadFrom(r)
buf.Truncate(int(n))
_ = r.Close()
res, _ := io.ReadAll(buf)
bufPool.Put(buf)
gzipPool.Put(gz)
return res
}
// Gunzip decompresses a gzip compressed slice of bytes.
func Gunzip(data []byte) (out []byte, err error) {
gz, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
var n int64
n, _ = buf.ReadFrom(gz)
err = gz.Close()
buf.Truncate(int(n))
res, _ := io.ReadAll(buf)
bufPool.Put(buf)
return res, err
}
// B64e encodes the given slice of bytes into base64 standard encoding.
func B64e(in []byte) (out string) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
buf.Grow(base64.StdEncoding.EncodedLen(len(in)))
b64 := base64.NewEncoder(base64.StdEncoding, buf)
_, _ = b64.Write(in)
_ = b64.Close()
res := buf.Bytes()
bufPool.Put(buf)
return string(res)
}
// B64d decodes the given string into the original slice of bytes.
// Do note that this is for non critical tasks, it has no error handling for purposes of clean code.
func B64d(str string) (data []byte) {
if len(str) == 0 {
return nil
}
data, _ = base64.StdEncoding.DecodeString(str)
return data
}
// UnpackStr UNsafely unpacks (usually banners) that have been base64'd and then gzip'd.
func UnpackStr(encoded string) (string, error) {
one := B64d(encoded)
if len(one) == 0 {
return "", errors.New("0 length base64 decoding result")
}
dcytes, err := Gunzip(one)
if err != nil && !errors.Is(err, io.EOF) {
return "", err
}
return string(dcytes), nil
}

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"strconv"
@ -17,8 +16,6 @@ type Bridge struct {
Host string `json:"internalipaddress,omitempty"`
User string
ID string `json:"id,omitempty"`
client *http.Client
}
func (b *Bridge) getAPIPath(str ...string) (string, error) {
@ -66,7 +63,7 @@ func (b *Bridge) GetConfigContext(ctx context.Context) (*Config, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -136,7 +133,7 @@ func (b *Bridge) createUserWithContext(ctx context.Context, deviceType string, g
return nil, err
}
res, err := post(ctx, url, data, b.client)
res, err := post(ctx, url, data)
if err != nil {
return nil, err
}
@ -194,7 +191,7 @@ func (b *Bridge) UpdateConfigContext(ctx context.Context, c *Config) (*Response,
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -227,7 +224,7 @@ func (b *Bridge) DeleteUserContext(ctx context.Context, n string) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -258,7 +255,7 @@ func (b *Bridge) GetFullStateContext(ctx context.Context) (map[string]interface{
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -292,7 +289,7 @@ func (b *Bridge) GetGroupsContext(ctx context.Context) ([]Group, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -334,7 +331,7 @@ func (b *Bridge) GetGroupContext(ctx context.Context, i int) (*Group, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -370,7 +367,7 @@ func (b *Bridge) SetGroupStateContext(ctx context.Context, i int, l State) (*Res
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -409,7 +406,7 @@ func (b *Bridge) UpdateGroupContext(ctx context.Context, i int, l Group) (*Respo
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -447,7 +444,7 @@ func (b *Bridge) CreateGroupContext(ctx context.Context, g Group) (*Response, er
return nil, err
}
res, err := post(ctx, url, data, b.client)
res, err := post(ctx, url, data)
if err != nil {
return nil, err
}
@ -481,7 +478,7 @@ func (b *Bridge) DeleteGroupContext(ctx context.Context, i int) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -517,7 +514,7 @@ func (b *Bridge) GetLightsContext(ctx context.Context) ([]Light, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -559,7 +556,7 @@ func (b *Bridge) GetLightContext(ctx context.Context, i int) (*Light, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return light, err
}
@ -588,7 +585,7 @@ func (b *Bridge) IdentifyLightContext(ctx context.Context, i int) (*Response, er
if err != nil {
return nil, err
}
res, err := put(ctx, url, []byte(`{"alert":"select"}`), b.client)
res, err := put(ctx, url, []byte(`{"alert":"select"}`))
if err != nil {
return nil, err
}
@ -629,7 +626,7 @@ func (b *Bridge) SetLightStateContext(ctx context.Context, i int, l State) (*Res
if err != nil {
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -665,7 +662,7 @@ func (b *Bridge) FindLightsContext(ctx context.Context) (*Response, error) {
return nil, err
}
res, err := post(ctx, url, nil, b.client)
res, err := post(ctx, url, nil)
if err != nil {
return nil, err
}
@ -699,7 +696,7 @@ func (b *Bridge) GetNewLightsContext(ctx context.Context) (*NewLight, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -742,7 +739,7 @@ func (b *Bridge) DeleteLightContext(ctx context.Context, i int) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -779,7 +776,7 @@ func (b *Bridge) UpdateLightContext(ctx context.Context, i int, light Light) (*R
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -818,7 +815,7 @@ func (b *Bridge) GetResourcelinksContext(ctx context.Context) ([]*Resourcelink,
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -860,7 +857,7 @@ func (b *Bridge) GetResourcelinkContext(ctx context.Context, i int) (*Resourceli
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -894,7 +891,7 @@ func (b *Bridge) CreateResourcelinkContext(ctx context.Context, s *Resourcelink)
return nil, err
}
res, err := post(ctx, url, data, b.client)
res, err := post(ctx, url, data)
if err != nil {
return nil, err
}
@ -932,7 +929,7 @@ func (b *Bridge) UpdateResourcelinkContext(ctx context.Context, i int, resourcel
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -966,7 +963,7 @@ func (b *Bridge) DeleteResourcelinkContext(ctx context.Context, i int) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -1002,7 +999,7 @@ func (b *Bridge) GetRulesContext(ctx context.Context) ([]*Rule, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1044,7 +1041,7 @@ func (b *Bridge) GetRuleContext(ctx context.Context, i int) (*Rule, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1078,7 +1075,7 @@ func (b *Bridge) CreateRuleContext(ctx context.Context, s *Rule) (*Response, err
return nil, err
}
res, err := post(ctx, url, data, b.client)
res, err := post(ctx, url, data)
if err != nil {
return nil, err
}
@ -1117,7 +1114,7 @@ func (b *Bridge) UpdateRuleContext(ctx context.Context, i int, rule *Rule) (*Res
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -1151,7 +1148,7 @@ func (b *Bridge) DeleteRuleContext(ctx context.Context, i int) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -1187,7 +1184,7 @@ func (b *Bridge) GetScenesContext(ctx context.Context) ([]Scene, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1223,7 +1220,7 @@ func (b *Bridge) GetSceneContext(ctx context.Context, i string) (*Scene, error)
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1263,7 +1260,7 @@ func (b *Bridge) UpdateSceneContext(ctx context.Context, id string, s *Scene) (*
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -1304,7 +1301,7 @@ func (b *Bridge) SetSceneLightStateContext(ctx context.Context, id string, iid i
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -1345,7 +1342,7 @@ func (b *Bridge) RecallSceneContext(ctx context.Context, id string, gid int) (*R
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -1383,7 +1380,7 @@ func (b *Bridge) CreateSceneContext(ctx context.Context, s *Scene) (*Response, e
return nil, err
}
res, err := post(ctx, url, data, b.client)
res, err := post(ctx, url, data)
if err != nil {
return nil, err
}
@ -1416,7 +1413,7 @@ func (b *Bridge) DeleteSceneContext(ctx context.Context, id string) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -1452,7 +1449,7 @@ func (b *Bridge) GetSchedulesContext(ctx context.Context) ([]*Schedule, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1494,7 +1491,7 @@ func (b *Bridge) GetScheduleContext(ctx context.Context, i int) (*Schedule, erro
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1528,7 +1525,7 @@ func (b *Bridge) CreateScheduleContext(ctx context.Context, s *Schedule) (*Respo
return nil, err
}
res, err := post(ctx, url, data, b.client)
res, err := post(ctx, url, data)
if err != nil {
return nil, err
}
@ -1567,7 +1564,7 @@ func (b *Bridge) UpdateScheduleContext(ctx context.Context, i int, schedule *Sch
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -1601,7 +1598,7 @@ func (b *Bridge) DeleteScheduleContext(ctx context.Context, i int) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -1637,7 +1634,7 @@ func (b *Bridge) GetSensorsContext(ctx context.Context) ([]Sensor, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1677,7 +1674,7 @@ func (b *Bridge) GetSensorContext(ctx context.Context, i int) (*Sensor, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return r, err
}
@ -1711,7 +1708,7 @@ func (b *Bridge) CreateSensorContext(ctx context.Context, s *Sensor) (*Response,
return nil, err
}
res, err := post(ctx, url, data, b.client)
res, err := post(ctx, url, data)
if err != nil {
return nil, err
}
@ -1747,7 +1744,7 @@ func (b *Bridge) FindSensorsContext(ctx context.Context) (*Response, error) {
return nil, err
}
res, err := post(ctx, url, nil, b.client)
res, err := post(ctx, url, nil)
if err != nil {
return nil, err
}
@ -1782,7 +1779,7 @@ func (b *Bridge) GetNewSensorsContext(ctx context.Context) (*NewSensor, error) {
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}
@ -1833,7 +1830,7 @@ func (b *Bridge) UpdateSensorContext(ctx context.Context, i int, sensor *Sensor)
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -1867,7 +1864,7 @@ func (b *Bridge) DeleteSensorContext(ctx context.Context, i int) error {
return err
}
res, err := del(ctx, url, b.client)
res, err := delete(ctx, url)
if err != nil {
return err
}
@ -1901,7 +1898,7 @@ func (b *Bridge) UpdateSensorConfigContext(ctx context.Context, i int, c interfa
return nil, err
}
res, err := put(ctx, url, data, b.client)
res, err := put(ctx, url, data)
if err != nil {
return nil, err
}
@ -1940,7 +1937,7 @@ func (b *Bridge) GetCapabilitiesContext(ctx context.Context) (*Capabilities, err
return nil, err
}
res, err := get(ctx, url, b.client)
res, err := get(ctx, url)
if err != nil {
return nil, err
}

View File

@ -85,7 +85,7 @@ func unmarshal(data []byte, v interface{}) error {
return nil
}
func get(ctx context.Context, url string, client *http.Client) ([]byte, error) {
func get(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
@ -94,6 +94,7 @@ func get(ctx context.Context, url string, client *http.Client) ([]byte, error) {
req = req.WithContext(ctx)
client := http.DefaultClient
res, err := client.Do(req)
if err != nil {
return nil, err
@ -110,7 +111,7 @@ func get(ctx context.Context, url string, client *http.Client) ([]byte, error) {
return body, nil
}
func put(ctx context.Context, url string, data []byte, client *http.Client) ([]byte, error) {
func put(ctx context.Context, url string, data []byte) ([]byte, error) {
body := strings.NewReader(string(data))
@ -123,6 +124,7 @@ func put(ctx context.Context, url string, data []byte, client *http.Client) ([]b
req.Header.Set(contentType, applicationJSON)
client := http.DefaultClient
res, err := client.Do(req)
if err != nil {
return nil, err
@ -139,7 +141,7 @@ func put(ctx context.Context, url string, data []byte, client *http.Client) ([]b
}
func post(ctx context.Context, url string, data []byte, client *http.Client) ([]byte, error) {
func post(ctx context.Context, url string, data []byte) ([]byte, error) {
body := strings.NewReader(string(data))
@ -152,6 +154,7 @@ func post(ctx context.Context, url string, data []byte, client *http.Client) ([]
req.Header.Set(contentType, applicationJSON)
client := http.DefaultClient
res, err := client.Do(req)
if err != nil {
return nil, err
@ -168,7 +171,8 @@ func post(ctx context.Context, url string, data []byte, client *http.Client) ([]
}
func del(ctx context.Context, url string, client *http.Client) ([]byte, error) {
func delete(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return nil, err
@ -178,6 +182,7 @@ func del(ctx context.Context, url string, client *http.Client) ([]byte, error) {
req.Header.Set(contentType, applicationJSON)
client := http.DefaultClient
res, err := client.Do(req)
if err != nil {
return nil, err
@ -244,6 +249,7 @@ func Discover() (*Bridge, error) {
// DiscoverContext performs a discovery on the network looking for bridges using https://www.meethue.com/api/nupnp service.
// DiscoverContext uses DiscoverAllContext() but only returns the first instance in the array of bridges if any.
func DiscoverContext(ctx context.Context) (*Bridge, error) {
b := &Bridge{}
bridges, err := DiscoverAllContext(ctx)
@ -263,40 +269,5 @@ func DiscoverContext(ctx context.Context) (*Bridge, error) {
// h may or may not be prefixed with http(s)://. For example http://192.168.1.20/ or 192.168.1.20.
// u is a username known to the bridge. Use Discover() and CreateUser() to create a user.
func New(h, u string) *Bridge {
return &Bridge{
Host: h,
User: u,
ID: "",
client: http.DefaultClient,
}
}
/*NewWithClient instantiates and returns a new Bridge with a custom HTTP client.
NewWithClient accepts the same parameters as New, but with an additional acceptance of an http.Client.
- h may or may not be prefixed with http(s)://. For example http://192.168.1.20/ or 192.168.1.20.
- u is a username known to the bridge. Use Discover() and CreateUser() to create a user.
- Difference between New and NewWithClient being the ability to implement your own http.RoundTripper for proxying.*/
func NewWithClient(h, u string, client *http.Client) *Bridge {
return &Bridge{
Host: h,
User: u,
ID: "",
client: client,
}
}
/*NewCustom instantiates and returns a new Bridge. NewCustom accepts:
- a raw JSON []byte slice as input for substantiating the Bridge type
- a custom HTTP client like NewWithClient that will be used to make API requests
Note that this is for advanced users, the other "New" functions may suit you better.*/
func NewCustom(raw []byte, host string, client *http.Client) (*Bridge, error) {
br := &Bridge{}
if err := json.Unmarshal(raw, br); err != nil {
return nil, err
}
br.Host = host
br.client = client
return br, nil
return &Bridge{h, u, ""}
}

1
vendor/modules.txt vendored
View File

@ -20,6 +20,7 @@ git.tcp.direct/Mirrors/go-prompt/internal/term
git.tcp.direct/kayos/common/entropy
git.tcp.direct/kayos/common/network
git.tcp.direct/kayos/common/pool
git.tcp.direct/kayos/common/squish
# git.tcp.direct/tcp.direct/database v0.0.0-20220829103039-b85255196bd1
## explicit; go 1.18
git.tcp.direct/tcp.direct/database