Compare commits

..

No commits in common. "599ff749d3739e7878e444367400d9eec38267d4" and "c3535a4a9b7f86f66ce08d692f136a7d2f972831" have entirely different histories.

137 changed files with 497 additions and 3949 deletions

View File

@ -1,4 +1 @@
RUN_APP_ARGS="" RUN_APP_ARGS=""
#EDGE_DOCUMENTSTORE_DSN="rpc://localhost:3001/documentstore?tenant=local&appId=%APPID%"
#EDGE_BLOBSTORE_DSN="rpc://localhost:3001/blobstore?tenant=local&appId=%APPID%"
#EDGE_SHARESTORE_DSN="rpc://localhost:3001/sharestore?tenant=local"

6
.gitignore vendored
View File

@ -5,9 +5,3 @@
*.sqlite *.sqlite
/.gitea-release /.gitea-release
/.edge /.edge
/data
.mktools/
/dist
/.chglog
/CHANGELOG.md
/storage-server.key

View File

@ -1,122 +0,0 @@
project_name: edge
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- id: edge-cli
binary: edge-cli
env:
- CGO_ENABLED=0
ldflags:
- -s
- -w
gcflags:
- -trimpath="${PWD}"
asmflags:
- -trimpath="${PWD}"
goos:
- linux
goarch:
- amd64
main: ./cmd/cli
- id: storage-server
binary: storage-server
env:
- CGO_ENABLED=0
ldflags:
- -s
- -w
gcflags:
- -trimpath="${PWD}"
asmflags:
- -trimpath="${PWD}"
goos:
- linux
goarch:
- amd64
main: ./cmd/storage-server
archives:
- id: edge-cli
builds: ["edge-cli"]
name_template: 'edge-cli_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
files:
- README.md
- CHANGELOG.md
- id: storage-server
builds: ["storage-server"]
name_template: 'storage-server_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
files:
- README.md
- CHANGELOG.md
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Version }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
nfpms:
- id: edge-cli
builds:
- "edge-cli"
package_name: edge-cli
homepage: https://forge.cadoles.com/arcad/edge
maintainer: William Petit <wpetit@cadoles.com>
description: |-
license: AGPL-3.0
formats:
- apk
- deb
- id: storage-server
builds:
- "storage-server"
package_name: storage-server
homepage: https://forge.cadoles.com/arcad/edge
maintainer: William Petit <wpetit@cadoles.com>
description: |-
license: AGPL-3.0
formats:
- apk
- deb
contents:
# Deb
- src: misc/packaging/systemd/storage-server.systemd.service
dst: /usr/lib/systemd/system/storage-server.service
packager: deb
- src: misc/packaging/systemd/storage-server.env
dst: /etc/storage-server/environ
type: config|noreplace
file_info:
mode: 0640
packager: deb
# APK
- src: misc/packaging/openrc/storage-server.openrc.sh
dst: /etc/init.d/storage-server
file_info:
mode: 0755
packager: apk
- src: misc/packaging/openrc/storage-server.conf
type: config|noreplace
dst: /etc/conf.d/storage-server
file_info:
mode: 0640
packager: apk
- dst: /var/lib/storage-server
type: dir
file_info:
mode: 0700
packager: apk
- dst: /var/log/storage-server
type: dir
file_info:
mode: 0750
packager: apk
scripts:
postinstall: "misc/packaging/common/postinstall-storage-server.sh"

View File

@ -6,17 +6,14 @@ GOTEST_ARGS ?= -short -timeout 60s
ESBUILD_VERSION ?= v0.17.5 ESBUILD_VERSION ?= v0.17.5
GIT_VERSION := $(shell git describe --always)
DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
APP_PATH ?= misc/client-sdk-testsuite/dist APP_PATH ?= misc/client-sdk-testsuite/dist
RUN_APP_ARGS ?= RUN_APP_ARGS ?=
RUN_STORAGE_SERVER_ARGS ?=
GORELEASER_VERSION ?= v1.21.2
GORELEASER_ARGS ?= release --snapshot --clean
SHELL := bash SHELL := bash
build: build-edge-cli build-client-sdk-test-app
build: build-cli build-storage-server build-client-sdk-test-app
watch: tools/modd/bin/modd watch: tools/modd/bin/modd
tools/modd/bin/modd tools/modd/bin/modd
@ -25,23 +22,17 @@ watch: tools/modd/bin/modd
test: test-go test: test-go
test-go: test-go:
go test -count=1 $(GOTEST_ARGS) ./... go test -v -count=1 $(GOTEST_ARGS) ./...
lint: lint:
golangci-lint run --enable-all $(LINT_ARGS) golangci-lint run --enable-all $(LINT_ARGS)
build-cli: build-sdk build-edge-cli: build-sdk
CGO_ENABLED=0 go build \ CGO_ENABLED=0 go build \
-v \ -v \
-o ./bin/cli \ -o ./bin/cli \
./cmd/cli ./cmd/cli
build-storage-server: build-sdk
CGO_ENABLED=0 go build \
-v \
-o ./bin/storage-server \
./cmd/storage-server
build-client-sdk-test-app: build-client-sdk-test-app:
cd misc/client-sdk-testsuite && $(MAKE) dist cd misc/client-sdk-testsuite && $(MAKE) dist
@ -77,31 +68,25 @@ node_modules:
run-app: .env run-app: .env
( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS ) ( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS )
run-storage-server: .env
( set -o allexport && source .env && set +o allexport && bin/storage-server run $$RUN_STORAGE_SERVER_ARGS )
.env: .env:
cp .env.dist .env cp .env.dist .env
gitea-release: tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh goreleaser build gitea-release: tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh build
mkdir -p .gitea-release mkdir -p .gitea-release
rm -rf .gitea-release/* rm -rf .gitea-release/*
cp dist/*.deb .gitea-release/ cp bin/cli .gitea-release/edge_cli_amd64
cp dist/*.tar.gz .gitea-release/
cp dist/*.apk .gitea-release/
cp CHANGELOG.md .gitea-release/
# Create client-sdk-testsuite package # Create client-sdk-testsuite package
tools/yq/bin/yq -i '.version = "$(MKT_PROJECT_VERSION)"' ./misc/client-sdk-testsuite/dist/manifest.yml tools/yq/bin/yq -i '.version = "$(FULL_VERSION)"' ./misc/client-sdk-testsuite/dist/manifest.yml
bin/cli app package -d ./misc/client-sdk-testsuite/dist -o .gitea-release .gitea-release/edge_cli_amd64 app package -d ./misc/client-sdk-testsuite/dist -o .gitea-release
GITEA_RELEASE_PROJECT="edge" \ GITEA_RELEASE_PROJECT="edge" \
GITEA_RELEASE_ORG="arcad" \ GITEA_RELEASE_ORG="arcad" \
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \ GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
GITEA_RELEASE_VERSION="$(MKT_PROJECT_VERSION)" \ GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
GITEA_RELEASE_NAME="$(MKT_PROJECT_VERSION)" \ GITEA_RELEASE_NAME="$(FULL_VERSION)" \
GITEA_RELEASE_COMMITISH_TARGET="$$(git rev-parse HEAD)" \ GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
GITEA_RELEASE_IS_DRAFT="false" \ GITEA_RELEASE_IS_DRAFT="false" \
GITEA_RELEASE_IS_PRERELEASE="true" \ GITEA_RELEASE_IS_PRERELEASE="true" \
GITEA_RELEASE_BODY="" \ GITEA_RELEASE_BODY="" \
@ -121,21 +106,3 @@ tools/yq/bin/yq:
tools/modd/bin/modd: tools/modd/bin/modd:
mkdir -p tools/modd/bin mkdir -p tools/modd/bin
GOBIN=$(PWD)/tools/modd/bin go install -mod=readonly github.com/cortesi/modd/cmd/modd@latest GOBIN=$(PWD)/tools/modd/bin go install -mod=readonly github.com/cortesi/modd/cmd/modd@latest
.PHONY: goreleaser
goreleaser: .mktools changelog
( set -o allexport && source .env && set +o allexport && curl -sfL https://goreleaser.com/static/run | VERSION="$(GORELEASER_VERSION)" GORELEASER_CURRENT_TAG="$(MKT_PROJECT_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) )
.PHONY: changelog
changelog: .mktools
$(MAKE) MKT_GIT_CHGLOG_ARGS='--next-tag $$(MKT_PROJECT_VERSION) --tag-filter-pattern $$(MKT_PROJECT_VERSION_CHANNEL) --output CHANGELOG.md' mkt-changelog
.PHONY: mktools
mktools:
rm -rf .mktools
curl -q https://forge.cadoles.com/Cadoles/mktools/raw/branch/master/install.sh | $(SHELL)
.mktools:
$(MAKE) mktools
-include .mktools/*.mk

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -16,7 +17,6 @@ import (
"forge.cadoles.com/arcad/edge/pkg/bus" "forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/bus/memory" "forge.cadoles.com/arcad/edge/pkg/bus/memory"
appHTTP "forge.cadoles.com/arcad/edge/pkg/http" appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app" appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory" appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
@ -28,7 +28,9 @@ import (
"forge.cadoles.com/arcad/edge/pkg/module/fetch" "forge.cadoles.com/arcad/edge/pkg/module/fetch"
netModule "forge.cadoles.com/arcad/edge/pkg/module/net" netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share" shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
shareSqlite "forge.cadoles.com/arcad/edge/pkg/module/share/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage" "forge.cadoles.com/arcad/edge/pkg/storage"
storageSqlite "forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/bundle" "forge.cadoles.com/arcad/edge/pkg/bundle"
@ -43,16 +45,8 @@ import (
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id" _ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain" _ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
// Register storage drivers
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
) )
var dummySecret = []byte("not_so_secret")
func RunCommand() *cli.Command { func RunCommand() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "run", Name: "run",
@ -81,22 +75,14 @@ func RunCommand() *cli.Command {
Value: 0, Value: 0,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "blobstore-dsn", Name: "storage-file",
Usage: "use `DSN` for blob storage", Usage: "use `FILE` for SQLite storage database",
EnvVars: []string{"EDGE_BLOBSTORE_DSN"}, Value: ".edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
Value: "sqlite://.edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "documentstore-dsn", Name: "shared-resources-file",
Usage: "use `DSN` for document storage", Usage: "use `FILE` for SQLite shared resources database",
EnvVars: []string{"EDGE_DOCUMENTSTORE_DSN"}, Value: ".edge/shared-resources.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
Value: "sqlite://.edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
},
&cli.StringFlag{
Name: "sharestore-dsn",
Usage: "use `DSN` for share storage",
EnvVars: []string{"EDGE_SHARESTORE_DSN"},
Value: "sqlite://.edge/share.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "accounts-file", Name: "accounts-file",
@ -110,10 +96,9 @@ func RunCommand() *cli.Command {
logFormat := ctx.String("log-format") logFormat := ctx.String("log-format")
logLevel := ctx.Int("log-level") logLevel := ctx.Int("log-level")
blobstoreDSN := ctx.String("blobstore-dsn") storageFile := ctx.String("storage-file")
documentstoreDSN := ctx.String("documentstore-dsn")
shareStoreDSN := ctx.String("sharestore-dsn")
accountsFile := ctx.String("accounts-file") accountsFile := ctx.String("accounts-file")
sharedResourcesFile := ctx.String("shared-resources-file")
logger.SetFormat(logger.Format(logFormat)) logger.SetFormat(logger.Format(logFormat))
logger.SetLevel(logger.Level(logLevel)) logger.SetLevel(logger.Level(logLevel))
@ -159,7 +144,7 @@ func RunCommand() *cli.Command {
appCtx := logger.With(cmdCtx, logger.F("address", address)) appCtx := logger.With(cmdCtx, logger.F("address", address))
if err := runApp(appCtx, path, address, documentstoreDSN, blobstoreDSN, shareStoreDSN, accountsFile, appsRepository); err != nil { if err := runApp(appCtx, path, address, storageFile, accountsFile, appsRepository, sharedResourcesFile); err != nil {
logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err))) logger.Error(appCtx, "could not run app", logger.E(errors.WithStack(err)))
} }
}(p, port, idx) }(p, port, idx)
@ -172,7 +157,7 @@ func RunCommand() *cli.Command {
} }
} }
func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN, shareStoreDSN, accountsFile string, appRepository appModule.Repository) error { func runApp(ctx context.Context, path string, address string, storageFile string, accountsFile string, appRepository appModule.Repository, sharedResourcesFile string) error {
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not resolve path '%s'", path) return errors.Wrapf(err, "could not resolve path '%s'", path)
@ -197,17 +182,17 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN,
ctx = logger.With(ctx, logger.F("appID", manifest.ID)) ctx = logger.With(ctx, logger.F("appID", manifest.ID))
// Add auth handler // Add auth handler
key, err := jwtutil.NewSymmetricKey(dummySecret) key, err := dummyKey()
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
deps := &moduleDeps{} deps := &moduleDeps{}
funcs := []ModuleDepFunc{ funcs := []ModuleDepFunc{
initAppID(manifest),
initMemoryBus, initMemoryBus,
initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN, manifest.ID), initDatastores(storageFile, manifest.ID),
initAccounts(accountsFile, manifest.ID), initAccounts(accountsFile, manifest.ID),
initShareRepository(sharedResourcesFile),
initAppRepository(appRepository), initAppRepository(appRepository),
} }
@ -224,18 +209,17 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN,
appModule.Mount(appRepository), appModule.Mount(appRepository),
authModule.Mount( authModule.Mount(
authHTTP.NewLocalHandler( authHTTP.NewLocalHandler(
key, jwa.HS256, key,
jwa.HS256,
authHTTP.WithRoutePrefix("/auth"), authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(deps.Accounts...), authHTTP.WithAccounts(deps.Accounts...),
), ),
authModule.WithJWT(func() (jwk.Set, error) { authModule.WithJWT(dummyKeySet),
return jwtutil.NewSymmetricKeySet(dummySecret)
}),
), ),
), ),
appHTTP.WithHTTPMiddlewares( appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(key, jwa.HS256), authModuleMiddleware.AnonymousUser(
jwa.HS256, key,
),
), ),
) )
if err := handler.Load(bundle); err != nil { if err := handler.Load(bundle); err != nil {
@ -259,13 +243,13 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN,
} }
type moduleDeps struct { type moduleDeps struct {
AppID app.ID AppID app.ID
Bus bus.Bus Bus bus.Bus
DocumentStore storage.DocumentStore DocumentStore storage.DocumentStore
BlobStore storage.BlobStore BlobStore storage.BlobStore
AppRepository appModule.Repository AppRepository appModule.Repository
ShareStore share.Store ShareRepository shareModule.Repository
Accounts []authHTTP.LocalAccount Accounts []authHTTP.LocalAccount
} }
type ModuleDepFunc func(*moduleDeps) error type ModuleDepFunc func(*moduleDeps) error
@ -281,16 +265,44 @@ func getServerModules(deps *moduleDeps) []app.ServerModuleFactory {
module.StoreModuleFactory(deps.DocumentStore), module.StoreModuleFactory(deps.DocumentStore),
blob.ModuleFactory(deps.Bus, deps.BlobStore), blob.ModuleFactory(deps.Bus, deps.BlobStore),
authModule.ModuleFactory( authModule.ModuleFactory(
authModule.WithJWT(func() (jwk.Set, error) { authModule.WithJWT(dummyKeySet),
return jwtutil.NewSymmetricKeySet(dummySecret)
}),
), ),
appModule.ModuleFactory(deps.AppRepository), appModule.ModuleFactory(deps.AppRepository),
fetch.ModuleFactory(deps.Bus), fetch.ModuleFactory(deps.Bus),
shareModule.ModuleFactory(deps.AppID, deps.ShareStore), shareModule.ModuleFactory(deps.AppID, deps.ShareRepository),
} }
} }
var dummySecret = []byte("not_so_secret")
func dummyKey() (jwk.Key, error) {
key, err := jwk.FromRaw(dummySecret)
if err != nil {
return nil, errors.WithStack(err)
}
return key, nil
}
func dummyKeySet() (jwk.Set, error) {
key, err := dummyKey()
if err != nil {
return nil, errors.WithStack(err)
}
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
return nil, errors.WithStack(err)
}
set := jwk.NewSet()
if err := set.AddKey(key); err != nil {
return nil, errors.WithStack(err)
}
return set, nil
}
func ensureDir(path string) error { func ensureDir(path string) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -311,10 +323,10 @@ func loadLocalAccounts(path string) ([]authHTTP.LocalAccount, error) {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
data, err := os.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
if err := os.WriteFile(path, defaultAccounts, 0o640); err != nil { if err := ioutil.WriteFile(path, defaultAccounts, 0o640); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -412,13 +424,6 @@ func newAppRepository(host string, basePort uint64, manifests ...*app.Manifest)
) )
} }
func initAppID(manifest *app.Manifest) ModuleDepFunc {
return func(deps *moduleDeps) error {
deps.AppID = manifest.ID
return nil
}
}
func initAppRepository(repo appModule.Repository) ModuleDepFunc { func initAppRepository(repo appModule.Repository) ModuleDepFunc {
return func(deps *moduleDeps) error { return func(deps *moduleDeps) error {
deps.AppRepository = repo deps.AppRepository = repo
@ -432,32 +437,21 @@ func initMemoryBus(deps *moduleDeps) error {
return nil return nil
} }
func initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN string, appID app.ID) ModuleDepFunc { func initDatastores(storageFile string, appID app.ID) ModuleDepFunc {
return func(deps *moduleDeps) error { return func(deps *moduleDeps) error {
documentStoreDSN = injectAppID(documentStoreDSN, appID) storageFile = injectAppID(storageFile, appID)
documentStore, err := driver.NewDocumentStore(documentStoreDSN) if err := ensureDir(storageFile); err != nil {
return errors.WithStack(err)
}
db, err := storageSqlite.Open(storageFile)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
deps.DocumentStore = documentStore deps.DocumentStore = storageSqlite.NewDocumentStoreWithDB(db)
deps.BlobStore = storageSqlite.NewBlobStoreWithDB(db)
blobStoreDSN = injectAppID(blobStoreDSN, appID)
blobStore, err := driver.NewBlobStore(blobStoreDSN)
if err != nil {
return errors.WithStack(err)
}
deps.BlobStore = blobStore
shareStore, err := driver.NewShareStore(shareStoreDSN)
if err != nil {
return errors.WithStack(err)
}
deps.ShareStore = shareStore
return nil return nil
} }
@ -477,3 +471,17 @@ func initAccounts(accountsFile string, appID app.ID) ModuleDepFunc {
return nil return nil
} }
} }
func initShareRepository(shareRepositoryFile string) ModuleDepFunc {
return func(deps *moduleDeps) error {
if err := ensureDir(shareRepositoryFile); err != nil {
return errors.WithStack(err)
}
repo := shareSqlite.NewRepository(shareRepositoryFile)
deps.ShareRepository = repo
return nil
}
}

View File

@ -1,58 +0,0 @@
package auth
import (
"fmt"
"forge.cadoles.com/arcad/edge/cmd/storage-server/command/flag"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func NewToken() *cli.Command {
return &cli.Command{
Name: "new-token",
Usage: "Generate new authentication token",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "tenant",
Required: true,
},
flag.PrivateKey,
flag.PrivateKeySigningAlgorithm,
flag.PrivateKeyDefaultSize,
},
Action: func(ctx *cli.Context) error {
privateKeyFile := flag.GetPrivateKey(ctx)
signingAlgorithm := flag.GetSigningAlgorithm(ctx)
privateKeyDefaultSize := flag.GetPrivateKeyDefaultSize(ctx)
tenant := ctx.String("tenant")
if tenant == "" {
return errors.New("you must provide a value for --tenant flag")
}
privateKey, err := jwtutil.LoadOrGenerateKey(
privateKeyFile,
privateKeyDefaultSize,
)
if err != nil {
return errors.WithStack(err)
}
claims := map[string]any{
"tenant": tenant,
}
token, err := jwtutil.SignedToken(privateKey, jwa.SignatureAlgorithm(signingAlgorithm), claims)
if err != nil {
return errors.Wrap(err, "could not generate signed token")
}
fmt.Println(string(token))
return nil
},
}
}

View File

@ -1,15 +0,0 @@
package auth
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "auth",
Usage: "Auth related command",
Subcommands: []*cli.Command{
NewToken(),
},
}
}

View File

@ -1,43 +0,0 @@
package flag
import (
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/urfave/cli/v2"
)
const PrivateKeyFlagName = "private-key"
var PrivateKey = &cli.StringFlag{
Name: PrivateKeyFlagName,
EnvVars: []string{"STORAGE_SERVER_PRIVATE_KEY"},
Value: "storage-server.key",
TakesFile: true,
}
func GetPrivateKey(ctx *cli.Context) string {
return ctx.String(PrivateKeyFlagName)
}
const SigningAlgorithmFlagName = "signing-algorithm"
var PrivateKeySigningAlgorithm = &cli.StringFlag{
Name: SigningAlgorithmFlagName,
EnvVars: []string{"STORAGE_SERVER_SIGNING_ALGORITHM"},
Value: jwa.RS256.String(),
}
func GetSigningAlgorithm(ctx *cli.Context) string {
return ctx.String(SigningAlgorithmFlagName)
}
const PrivateKeyDefaultSizeFlagName = "private-key-default-size"
var PrivateKeyDefaultSize = &cli.IntFlag{
Name: PrivateKeyDefaultSizeFlagName,
EnvVars: []string{"STORAGE_SERVER_PRIVATE_KEY_DEFAULT_SIZE"},
Value: 2048,
}
func GetPrivateKeyDefaultSize(ctx *cli.Context) int {
return ctx.Int(PrivateKeyDefaultSizeFlagName)
}

View File

@ -1,48 +0,0 @@
package command
import (
"context"
"fmt"
"os"
"sort"
"github.com/urfave/cli/v2"
)
func Main(commands ...*cli.Command) {
ctx := context.Background()
app := &cli.App{
Name: "storage-server",
Usage: "Edge storage server",
Commands: commands,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
EnvVars: []string{"DEBUG"},
Value: false,
},
},
}
app.ExitErrHandler = func(ctx *cli.Context, err error) {
if err == nil {
return
}
debug := ctx.Bool("debug")
if !debug {
fmt.Printf("[ERROR] %v\n", err)
} else {
fmt.Printf("%+v", err)
}
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
if err := app.RunContext(ctx, os.Args); err != nil {
os.Exit(1)
}
}

View File

@ -1,283 +0,0 @@
package command
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/keegancsmith/rpc"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"gitlab.com/wpetit/goweb/logger"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Register storage drivers
"forge.cadoles.com/arcad/edge/cmd/storage-server/command/flag"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
func Run() *cli.Command {
return &cli.Command{
Name: "run",
Usage: "Run server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
EnvVars: []string{"STORAGE_SERVER_ADDRESS"},
Aliases: []string{"addr"},
Value: ":3001",
},
&cli.StringFlag{
Name: "blobstore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_BLOBSTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/blobstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.StringFlag{
Name: "documentstore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_DOCUMENTSTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/documentstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
&cli.StringFlag{
Name: "sharestore-dsn-pattern",
EnvVars: []string{"STORAGE_SERVER_SHARESTORE_DSN_PATTERN"},
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/sharestore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
},
flag.PrivateKey,
flag.PrivateKeySigningAlgorithm,
flag.PrivateKeyDefaultSize,
&cli.DurationFlag{
Name: "cache-ttl",
EnvVars: []string{"STORAGE_SERVER_CACHE_TTL"},
Value: time.Hour,
},
&cli.IntFlag{
Name: "cache-size",
EnvVars: []string{"STORAGE_SERVER_CACHE_SIZE"},
Value: 32,
},
},
Action: func(ctx *cli.Context) error {
addr := ctx.String("address")
blobStoreDSNPattern := ctx.String("blobstore-dsn-pattern")
documentStoreDSNPattern := ctx.String("documentstore-dsn-pattern")
shareStoreDSNPattern := ctx.String("sharestore-dsn-pattern")
cacheSize := ctx.Int("cache-size")
cacheTTL := ctx.Duration("cache-ttl")
privateKeyFile := flag.GetPrivateKey(ctx)
signingAlgorithm := flag.GetSigningAlgorithm(ctx)
privateKeyDefaultSize := flag.GetPrivateKeyDefaultSize(ctx)
router := chi.NewRouter()
privateKey, err := jwtutil.LoadOrGenerateKey(
privateKeyFile,
privateKeyDefaultSize,
)
if err != nil {
return errors.WithStack(err)
}
publicKey, err := privateKey.PublicKey()
if err != nil {
return errors.WithStack(err)
}
getBlobStoreServer := createGetCachedStoreServer(
func(dsn string) (storage.BlobStore, error) {
return driver.NewBlobStore(dsn)
},
func(store storage.BlobStore) *rpc.Server {
return server.NewBlobStoreServer(store)
},
)
getShareStoreServer := createGetCachedStoreServer(
func(dsn string) (share.Store, error) {
return driver.NewShareStore(dsn)
},
func(store share.Store) *rpc.Server {
return server.NewShareStoreServer(store)
},
)
getDocumentStoreServer := createGetCachedStoreServer(
func(dsn string) (storage.DocumentStore, error) {
return driver.NewDocumentStore(dsn)
},
func(store storage.DocumentStore) *rpc.Server {
return server.NewDocumentStoreServer(store)
},
)
router.Use(middleware.RealIP)
router.Use(middleware.Logger)
router.Use(authenticate(publicKey, jwa.SignatureAlgorithm(signingAlgorithm)))
router.Handle("/blobstore", createStoreHandler(getBlobStoreServer, blobStoreDSNPattern, true, cacheSize, cacheTTL))
router.Handle("/documentstore", createStoreHandler(getDocumentStoreServer, documentStoreDSNPattern, true, cacheSize, cacheTTL))
router.Handle("/sharestore", createStoreHandler(getShareStoreServer, shareStoreDSNPattern, false, cacheSize, cacheTTL))
if err := http.ListenAndServe(addr, router); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}
type getRPCServerFunc func(cacheSize int, cacheTTL time.Duration, tenant, appID, dsnPattern string) (*rpc.Server, error)
func createGetCachedStoreServer[T any](storeFactory func(dsn string) (T, error), serverFactory func(store T) *rpc.Server) getRPCServerFunc {
var (
cache *expirable.LRU[string, *rpc.Server]
initCache sync.Once
)
return func(cacheSize int, cacheTTL time.Duration, tenant, appID, dsnPattern string) (*rpc.Server, error) {
initCache.Do(func() {
cache = expirable.NewLRU[string, *rpc.Server](cacheSize, nil, cacheTTL)
})
key := fmt.Sprintf("%s:%s", tenant, appID)
storeServer, _ := cache.Get(key)
if storeServer != nil {
return storeServer, nil
}
dsn := strings.ReplaceAll(dsnPattern, "%TENANT%", tenant)
dsn = strings.ReplaceAll(dsn, "%APPID%", appID)
store, err := storeFactory(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
storeServer = serverFactory(store)
cache.Add(key, storeServer)
return storeServer, nil
}
}
func createStoreHandler(getStoreServer getRPCServerFunc, dsnPattern string, appIDRequired bool, cacheSize int, cacheTTL time.Duration) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tenant, ok := ctx.Value("tenant").(string)
if !ok || tenant == "" {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
appID := r.URL.Query().Get("appId")
if appIDRequired && appID == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
server, err := getStoreServer(cacheSize, cacheTTL, tenant, appID, dsnPattern)
if err != nil {
logger.Error(r.Context(), "could not retrieve store server", logger.E(errors.WithStack(err)), logger.F("tenant", tenant))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
server.ServeHTTP(w, r)
})
}
func authenticate(privateKey jwk.Key, signingAlgorithm jwa.SignatureAlgorithm) func(http.Handler) http.Handler {
var (
createKeySet sync.Once
err error
getKeySet jwtutil.GetKeySetFunc
)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
createKeySet.Do(func() {
err = privateKey.Set(jwk.AlgorithmKey, signingAlgorithm)
if err != nil {
return
}
var keySet jwk.Set
keySet, err = jwtutil.NewKeySet(privateKey)
if err != nil {
return
}
getKeySet = func() (jwk.Set, error) {
return keySet, nil
}
})
if err != nil {
logger.Error(ctx, "could not create keyset accessor", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
token, err := jwtutil.FindToken(r, getKeySet, jwtutil.WithFinders(
jwtutil.FindTokenFromQueryString("token"),
))
if err != nil {
logger.Error(ctx, "could not find jwt token", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
tokenMap, err := token.AsMap(ctx)
if err != nil {
logger.Error(ctx, "could not transform token to map", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
rawTenant, exists := tokenMap["tenant"]
if !exists {
logger.Warn(ctx, "could not find tenant claim", logger.F("token", token))
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
tenant, ok := rawTenant.(string)
if !ok {
logger.Warn(ctx, "unexpected tenant claim value", logger.F("token", token))
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
r = r.WithContext(context.WithValue(ctx, "tenant", tenant))
next.ServeHTTP(w, r)
})
}
}

View File

@ -1,13 +0,0 @@
package main
import (
"forge.cadoles.com/arcad/edge/cmd/storage-server/command"
"forge.cadoles.com/arcad/edge/cmd/storage-server/command/auth"
)
func main() {
command.Main(
command.Run(),
auth.Root(),
)
}

4
go.mod
View File

@ -3,9 +3,7 @@ module forge.cadoles.com/arcad/edge
go 1.19 go 1.19
require ( require (
github.com/hashicorp/golang-lru/v2 v2.0.6
github.com/hashicorp/mdns v1.0.5 github.com/hashicorp/mdns v1.0.5
github.com/keegancsmith/rpc v1.3.0
github.com/lestrrat-go/jwx/v2 v2.0.8 github.com/lestrrat-go/jwx/v2 v2.0.8
modernc.org/sqlite v1.20.4 modernc.org/sqlite v1.20.4
) )
@ -45,7 +43,7 @@ require (
github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/chi/v5 v5.0.8
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/igm/sockjs-go/v3 v3.0.2 github.com/igm/sockjs-go/v3 v3.0.2
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect

4
go.sum
View File

@ -188,8 +188,6 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM=
github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0= github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
@ -203,8 +201,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keegancsmith/rpc v1.3.0 h1:wGWOpjcNrZaY8GDYZJfvyxmlLljm3YQWF+p918DXtDk=
github.com/keegancsmith/rpc v1.3.0/go.mod h1:6O2xnOGjPyvIPbvp0MdrOe5r6cu1GZ4JoTzpzDhWeo0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=

View File

@ -24,7 +24,6 @@
mocha.checkLeaks(); mocha.checkLeaks();
</script> </script>
<script src="/edge/sdk/client.js"></script> <script src="/edge/sdk/client.js"></script>
<script src="/test/util.js"></script>
<script src="/test/client-sdk.js"></script> <script src="/test/client-sdk.js"></script>
<script src="/test/auth-module.js"></script> <script src="/test/auth-module.js"></script>
<script src="/test/net-module.js"></script> <script src="/test/net-module.js"></script>
@ -32,7 +31,6 @@
<script src="/test/file-module.js"></script> <script src="/test/file-module.js"></script>
<script src="/test/app-module.js"></script> <script src="/test/app-module.js"></script>
<script src="/test/fetch-module.js"></script> <script src="/test/fetch-module.js"></script>
<script src="/test/share-module.js"></script>
<script class="mocha-exec"> <script class="mocha-exec">
mocha.run(); mocha.run();
@ -46,7 +44,6 @@
.setItem('file-module', 'File Module', { linkUrl: '/?grep=File%20Module', order: 6}) .setItem('file-module', 'File Module', { linkUrl: '/?grep=File%20Module', order: 6})
.setItem('app-module', 'App Module', { linkUrl: '/?grep=App%20Module' , order: 7}) .setItem('app-module', 'App Module', { linkUrl: '/?grep=App%20Module' , order: 7})
.setItem('fetch-module', 'Fetch Module', { linkUrl: '/?grep=Fetch%20Module' , order: 8}) .setItem('fetch-module', 'Fetch Module', { linkUrl: '/?grep=Fetch%20Module' , order: 8})
.setItem('share-module', 'Share Module', { linkUrl: '/?grep=Share%20Module' , order: 9})
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,29 +0,0 @@
describe('Share Module', function() {
before(() => {
return Edge.Client.connect();
});
after(() => {
Edge.Client.disconnect();
});
it('should create a new resource and find it', async () => {
const resource = await TestUtil.serverSideCall('share', 'upsertResource', 'my-resource', { name: "color", type: "text", value: "red" });
chai.assert.isNotNull(resource);
chai.assert.equal(resource.origin, 'edge.sdk.client.test')
const results = await TestUtil.serverSideCall('share', 'findResources', 'color', 'text');
chai.assert.isAbove(results.length, 0);
const createdResource = results.find(res => {
return res.origin === 'edge.sdk.client.test' &&
res.attributes.find(attr => attr.name === 'color' && attr.type === 'text')
})
chai.assert.isNotNull(createdResource)
console.log(createdResource)
});
});

View File

@ -1,7 +0,0 @@
(function(TestUtil) {
TestUtil.serverSideCall = (module, func, ...args) => {
return Edge.Client.rpc('serverSideCall', { module, func, args })
}
console.log(TestUtil)
}(globalThis.TestUtil = globalThis.TestUtil || {}));

View File

@ -15,8 +15,6 @@ function onInit() {
rpc.register("listApps"); rpc.register("listApps");
rpc.register("getApp"); rpc.register("getApp");
rpc.register("getAppUrl"); rpc.register("getAppUrl");
rpc.register("serverSideCall", serverSideCall)
} }
// Called for each client message // Called for each client message
@ -106,8 +104,3 @@ function getAppUrl(ctx, params) {
function onClientFetch(ctx, url, remoteAddr) { function onClientFetch(ctx, url, remoteAddr) {
return { allow: url === 'http://example.com' }; return { allow: url === 'http://example.com' };
} }
function serverSideCall(ctx, params) {
console.log("Calling %s.%s(args...)", params.module, params.func)
return globalThis[params.module][params.func].call(null, ctx, ...params.args);
}

View File

@ -1,75 +0,0 @@
#!/bin/sh
use_systemctl="True"
systemd_version=0
if ! command -V systemctl >/dev/null 2>&1; then
use_systemctl="False"
else
systemd_version=$(systemctl --version | head -1 | cut -d ' ' -f 2)
fi
service_name=storage-server
cleanup() {
if [ "${use_systemctl}" = "False" ]; then
rm -f /usr/lib/systemd/system/${service_name}.service
else
rm -f /etc/chkconfig/${service_name}
rm -f /etc/init.d/${service_name}
fi
}
cleanInstall() {
printf "\033[32m Post Install of an clean install\033[0m\n"
if [ "${use_systemctl}" = "False" ]; then
if command -V chkconfig >/dev/null 2>&1; then
chkconfig --add ${service_name}
fi
service ${service_name} restart || :
else
if [[ "${systemd_version}" -lt 231 ]]; then
printf "\033[31m systemd version %s is less then 231, fixing the service file \033[0m\n" "${systemd_version}"
sed -i "s/=+/=/g" /usr/lib/systemd/system/${service_name}.service
fi
printf "\033[32m Reload the service unit from disk\033[0m\n"
systemctl daemon-reload || :
printf "\033[32m Unmask the service\033[0m\n"
systemctl unmask ${service_name} || :
printf "\033[32m Set the preset flag for the service unit\033[0m\n"
systemctl preset ${service_name} || :
printf "\033[32m Set the enabled flag for the service unit\033[0m\n"
systemctl enable ${service_name} || :
systemctl restart ${service_name} || :
fi
}
upgrade() {
printf "\033[32m Post Install of an upgrade\033[0m\n"
systemctl daemon-reload || :
systemctl restart ${service_name} || :
}
# Step 2, check if this is a clean install or an upgrade
action="$1"
if [ "$1" = "configure" ] && [ -z "$2" ]; then
action="install"
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
action="upgrade"
fi
case "$action" in
"1" | "install")
cleanInstall
;;
"2" | "upgrade")
printf "\033[32m Post Install of an upgrade\033[0m\n"
upgrade
;;
*)
printf "\033[32m Alpine\033[0m"
cleanInstall
;;
esac
cleanup

View File

@ -1,9 +0,0 @@
export STORAGE_SERVER_ADDRESS=:3001
export STORAGE_SERVER_BLOBSTORE_DSN_PATTERN="sqlite:///var/lib/storage-server/data/%TENANT%/%APPID%/blobstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
export STORAGE_SERVER_DOCUMENTSTORE_DSN_PATTERN="sqlite:///var/lib/storage-server/data/%TENANT%/%APPID%/documentstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
export STORAGE_SERVER_SHARESTORE_DSN_PATTERN="sqlite:///var/lib/storage-server/data/%TENANT%/sharestore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
export STORAGE_SERVER_PRIVATE_KEY="/var/lib/storage-server/storage-server.key"
export STORAGE_SERVER_PRIVATE_KEY_DEFAULT_SIZE="2048"
export STORAGE_SERVER_SIGNING_ALGORITHM="RS256"
export STORAGE_SERVER_CACHE_TTL=1h
export STORAGE_SERVER_CACHE_SIZE=32

View File

@ -1,11 +0,0 @@
#!/sbin/openrc-run
command="/usr/bin/storage-server"
command_args=""
supervisor=supervise-daemon
output_log="/var/log/storage-server.log"
error_log="$output_log"
depend() {
need net
}

View File

@ -1,9 +0,0 @@
STORAGE_SERVER_ADDRESS=:3001
STORAGE_SERVER_BLOBSTORE_DSN_PATTERN="sqlite:///var/lib/storage-server/data/%TENANT%/%APPID%/blobstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
STORAGE_SERVER_DOCUMENTSTORE_DSN_PATTERN="sqlite:///var/lib/storage-server/data/%TENANT%/%APPID%/documentstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
STORAGE_SERVER_SHARESTORE_DSN_PATTERN="sqlite:///var/lib/storage-server/data/%TENANT%/sharestore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
STORAGE_SERVER_PRIVATE_KEY="/var/lib/storage-server/storage-server.key"
STORAGE_SERVER_PRIVATE_KEY_DEFAULT_SIZE="2048"
STORAGE_SERVER_SIGNING_ALGORITHM="RS256"
STORAGE_SERVER_CACHE_TTL=1h
STORAGE_SERVER_CACHE_SIZE=32

View File

@ -1,35 +0,0 @@
[Unit]
Description=storage-server service
After=network.target
[Service]
Type=simple
Restart=on-failure
EnvironmentFile=/etc/storage-server/environ
ExecStart=/usr/bin/storage-server
EnvironmentFile=/etc/storage-server/environ
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
PrivateUsers=yes
DynamicUser=yes
StateDirectory=storage-server
DevicePolicy=closed
ProtectSystem=true
ProtectHome=read-only
ProtectKernelLogs=yes
ProtectProc=invisible
ProtectClock=yes
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
MemoryDenyWriteExecute=yes
LockPersonality=yes
CapabilityBoundingSet=~CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_NET_ADMIN CAP_WAKE_ALARM CAP_SYS_TTY_CONFIG
[Install]
WantedBy=multi-user.target

View File

@ -2,17 +2,13 @@
**/*.tmpl **/*.tmpl
pkg/sdk/client/src/**/*.js pkg/sdk/client/src/**/*.js
pkg/sdk/client/src/**/*.ts pkg/sdk/client/src/**/*.ts
misc/client-sdk-testsuite/dist/server/*.js misc/client-sdk-testsuite/src/**/*
modd.conf modd.conf
{ {
prep: make build-sdk build-cli build-storage-server prep: make build-sdk
daemon: make run-app
daemon: make run-storage-server
}
misc/client-sdk-testsuite/src/**/*
{
prep: make build-client-sdk-test-app prep: make build-client-sdk-test-app
prep: make build
daemon: make run-app
} }
**/*.go { **/*.go {

View File

@ -1,71 +0,0 @@
package jwtutil
import (
"crypto/rand"
"crypto/rsa"
"os"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
)
func LoadOrGenerateKey(path string, defaultKeySize int) (jwk.Key, error) {
key, err := LoadKey(path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
key, err = GenerateKey(defaultKeySize)
if err != nil {
return nil, errors.WithStack(err)
}
if err := SaveKey(path, key); err != nil {
return nil, errors.WithStack(err)
}
}
return key, nil
}
func LoadKey(path string) (jwk.Key, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, errors.WithStack(err)
}
key, err := jwk.ParseKey(data, jwk.WithPEM(true))
if err != nil {
return nil, errors.WithStack(err)
}
return key, nil
}
func SaveKey(path string, key jwk.Key) error {
data, err := jwk.Pem(key)
if err != nil {
return errors.WithStack(err)
}
if err := os.WriteFile(path, data, os.FileMode(0600)); err != nil {
return errors.WithStack(err)
}
return nil
}
func GenerateKey(keySize int) (jwk.Key, error) {
rsaKey, err := rsa.GenerateKey(rand.Reader, keySize)
if err != nil {
return nil, errors.WithStack(err)
}
key, err := jwk.FromRaw(rsaKey)
if err != nil {
return nil, errors.WithStack(err)
}
return key, nil
}

View File

@ -1,52 +0,0 @@
package jwtutil
import (
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
)
func NewKeySet(keys ...jwk.Key) (jwk.Set, error) {
set := jwk.NewSet()
for _, k := range keys {
if err := set.AddKey(k); err != nil {
return nil, errors.WithStack(err)
}
}
return set, nil
}
func NewSymmetricKey(secret []byte) (jwk.Key, error) {
key, err := jwk.FromRaw(secret)
if err != nil {
return nil, errors.WithStack(err)
}
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
return nil, errors.WithStack(err)
}
return key, nil
}
func NewSymmetricKeySet(secrets ...[]byte) (jwk.Set, error) {
keys := make([]jwk.Key, len(secrets))
for idx, sec := range secrets {
key, err := NewSymmetricKey(sec)
if err != nil {
return nil, errors.WithStack(err)
}
keys[idx] = key
}
keySet, err := NewKeySet(keys...)
if err != nil {
return nil, errors.WithStack(err)
}
return keySet, nil
}

View File

@ -1,123 +0,0 @@
package jwtutil
import (
"net/http"
"strings"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/pkg/errors"
)
type TokenFinderFunc func(r *http.Request) (string, error)
type FindTokenOptions struct {
Finders []TokenFinderFunc
}
type FindTokenOptionFunc func(*FindTokenOptions)
type GetKeySetFunc func() (jwk.Set, error)
func WithFinders(finders ...TokenFinderFunc) FindTokenOptionFunc {
return func(opts *FindTokenOptions) {
opts.Finders = finders
}
}
func NewFindTokenOptions(funcs ...FindTokenOptionFunc) *FindTokenOptions {
opts := &FindTokenOptions{
Finders: []TokenFinderFunc{
FindTokenFromAuthorizationHeader,
},
}
for _, fn := range funcs {
fn(opts)
}
return opts
}
func FindTokenFromAuthorizationHeader(r *http.Request) (string, error) {
authorization := r.Header.Get("Authorization")
// Retrieve token from Authorization header
rawToken := strings.TrimPrefix(authorization, "Bearer ")
return rawToken, nil
}
func FindTokenFromQueryString(name string) TokenFinderFunc {
return func(r *http.Request) (string, error) {
return r.URL.Query().Get(name), nil
}
}
func FindTokenFromCookie(cookieName string) TokenFinderFunc {
return func(r *http.Request) (string, error) {
cookie, err := r.Cookie(cookieName)
if err != nil && !errors.Is(err, http.ErrNoCookie) {
return "", errors.WithStack(err)
}
if cookie == nil {
return "", nil
}
return cookie.Value, nil
}
}
func FindRawToken(r *http.Request, funcs ...FindTokenOptionFunc) (string, error) {
opts := NewFindTokenOptions(funcs...)
var rawToken string
var err error
for _, find := range opts.Finders {
rawToken, err = find(r)
if err != nil {
return "", errors.WithStack(err)
}
if rawToken == "" {
continue
}
break
}
if rawToken == "" {
return "", errors.WithStack(ErrUnauthenticated)
}
return rawToken, nil
}
func FindToken(r *http.Request, getKeySet GetKeySetFunc, funcs ...FindTokenOptionFunc) (jwt.Token, error) {
rawToken, err := FindRawToken(r, funcs...)
if err != nil {
return nil, errors.WithStack(err)
}
keySet, err := getKeySet()
if err != nil {
return nil, errors.WithStack(err)
}
if keySet == nil {
return nil, errors.WithStack(ErrNoKeySet)
}
token, err := jwt.Parse([]byte(rawToken),
jwt.WithKeySet(keySet, jws.WithRequireKid(false)),
jwt.WithValidate(true),
)
if err != nil {
return nil, errors.WithStack(err)
}
return token, nil
}

View File

@ -1,6 +1,5 @@
package jwtutil package auth
import "errors" import "errors"
var ErrUnauthenticated = errors.New("unauthenticated") var ErrUnauthenticated = errors.New("unauthenticated")
var ErrNoKeySet = errors.New("no keyset")

View File

@ -7,9 +7,9 @@ import (
_ "embed" _ "embed"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/auth" "forge.cadoles.com/arcad/edge/pkg/module/auth"
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd" "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
"forge.cadoles.com/arcad/edge/pkg/module/auth/jwt"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
@ -31,12 +31,12 @@ func init() {
} }
type LocalHandler struct { type LocalHandler struct {
router chi.Router router chi.Router
key jwk.Key algo jwa.KeyAlgorithm
signingAlgorithm jwa.SignatureAlgorithm key jwk.Key
getCookieDomain GetCookieDomainFunc getCookieDomain GetCookieDomainFunc
cookieDuration time.Duration cookieDuration time.Duration
accounts map[string]LocalAccount accounts map[string]LocalAccount
} }
func (h *LocalHandler) initRouter(prefix string) { func (h *LocalHandler) initRouter(prefix string) {
@ -113,7 +113,7 @@ func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
account.Claims[auth.ClaimIssuer] = "local" account.Claims[auth.ClaimIssuer] = "local"
token, err := jwtutil.SignedToken(h.key, h.signingAlgorithm, account.Claims) token, err := jwt.GenerateSignedToken(h.algo, h.key, account.Claims)
if err != nil { if err != nil {
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -182,18 +182,18 @@ func (h *LocalHandler) authenticate(username, password string) (*LocalAccount, e
return &account, nil return &account, nil
} }
func NewLocalHandler(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...LocalHandlerOptionFunc) *LocalHandler { func NewLocalHandler(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...LocalHandlerOptionFunc) *LocalHandler {
opts := defaultLocalHandlerOptions() opts := defaultLocalHandlerOptions()
for _, fn := range funcs { for _, fn := range funcs {
fn(opts) fn(opts)
} }
handler := &LocalHandler{ handler := &LocalHandler{
key: key, algo: algo,
signingAlgorithm: signingAlgorithm, key: key,
accounts: toAccountsMap(opts.Accounts), accounts: toAccountsMap(opts.Accounts),
getCookieDomain: opts.GetCookieDomain, getCookieDomain: opts.GetCookieDomain,
cookieDuration: opts.CookieDuration, cookieDuration: opts.CookieDuration,
} }
handler.initRouter(opts.RoutePrefix) handler.initRouter(opts.RoutePrefix)

118
pkg/module/auth/jwt.go Normal file
View File

@ -0,0 +1,118 @@
package auth
import (
"context"
"net/http"
"strings"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/pkg/errors"
)
const (
CookieName string = "edge-auth"
)
type GetKeySetFunc func() (jwk.Set, error)
func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
return func(o *Option) {
o.GetClaims = func(ctx context.Context, r *http.Request, names ...string) ([]string, error) {
claim, err := getClaims[string](r, getKeySet, names...)
if err != nil {
return nil, errors.WithStack(err)
}
return claim, nil
}
}
}
func FindRawToken(r *http.Request) (string, error) {
authorization := r.Header.Get("Authorization")
// Retrieve token from Authorization header
rawToken := strings.TrimPrefix(authorization, "Bearer ")
// Retrieve token from ?edge-auth=<value>
if rawToken == "" {
rawToken = r.URL.Query().Get(CookieName)
}
if rawToken == "" {
cookie, err := r.Cookie(CookieName)
if err != nil && !errors.Is(err, http.ErrNoCookie) {
return "", errors.WithStack(err)
}
if cookie != nil {
rawToken = cookie.Value
}
}
if rawToken == "" {
return "", errors.WithStack(ErrUnauthenticated)
}
return rawToken, nil
}
func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
rawToken, err := FindRawToken(r)
if err != nil {
return nil, errors.WithStack(err)
}
keySet, err := getKeySet()
if err != nil {
return nil, errors.WithStack(err)
}
if keySet == nil {
return nil, errors.New("no keyset")
}
token, err := jwt.Parse([]byte(rawToken),
jwt.WithKeySet(keySet, jws.WithRequireKid(false)),
jwt.WithValidate(true),
)
if err != nil {
return nil, errors.WithStack(err)
}
return token, nil
}
func getClaims[T any](r *http.Request, getKeySet GetKeySetFunc, names ...string) ([]T, error) {
token, err := FindToken(r, getKeySet)
if err != nil {
return nil, errors.WithStack(err)
}
ctx := r.Context()
mapClaims, err := token.AsMap(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
claims := make([]T, len(names))
for idx, n := range names {
rawClaim, exists := mapClaims[n]
if !exists {
continue
}
claim, ok := rawClaim.(T)
if !ok {
return nil, errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", n, new(T), rawClaim)
}
claims[idx] = claim
}
return claims, nil
}

View File

@ -1,4 +1,4 @@
package jwtutil package jwt
import ( import (
"time" "time"
@ -6,32 +6,27 @@ import (
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt" "github.com/lestrrat-go/jwx/v2/jwt"
"github.com/oklog/ulid/v2"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func SignedToken(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, claims map[string]any) ([]byte, error) { func GenerateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) {
token := jwt.New() token := jwt.New()
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil { if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
if err := token.Set(jwt.JwtIDKey, ulid.Make().String()); err != nil {
return nil, errors.WithStack(err)
}
for key, value := range claims { for key, value := range claims {
if err := token.Set(key, value); err != nil { if err := token.Set(key, value); err != nil {
return nil, errors.Wrapf(err, "could not set claim '%s' with value '%v'", key, value) return nil, errors.Wrapf(err, "could not set claim '%s' with value '%v'", key, value)
} }
} }
if err := token.Set(jwk.AlgorithmKey, signingAlgorithm); err != nil { if err := token.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
rawToken, err := jwt.Sign(token, jwt.WithKey(signingAlgorithm, key)) rawToken, err := jwt.Sign(token, jwt.WithKey(algo, key))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }

View File

@ -7,8 +7,8 @@ import (
"net/http" "net/http"
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/auth" "forge.cadoles.com/arcad/edge/pkg/module/auth"
"forge.cadoles.com/arcad/edge/pkg/module/auth/jwt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
@ -18,7 +18,7 @@ import (
const AnonIssuer = "anon" const AnonIssuer = "anon"
func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler { func AnonymousUser(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler {
opts := defaultAnonymousUserOptions() opts := defaultAnonymousUserOptions()
for _, fn := range funcs { for _, fn := range funcs {
fn(opts) fn(opts)
@ -26,11 +26,7 @@ func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs .
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
handler := func(w http.ResponseWriter, r *http.Request) { handler := func(w http.ResponseWriter, r *http.Request) {
rawToken, err := jwtutil.FindRawToken(r, jwtutil.WithFinders( rawToken, err := auth.FindRawToken(r)
jwtutil.FindTokenFromAuthorizationHeader,
jwtutil.FindTokenFromQueryString(auth.CookieName),
jwtutil.FindTokenFromCookie(auth.CookieName),
))
// If request already has a raw token, we do nothing // If request already has a raw token, we do nothing
if rawToken != "" && err == nil { if rawToken != "" && err == nil {
@ -66,7 +62,7 @@ func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs .
auth.ClaimEdgeTenant: opts.Tenant, auth.ClaimEdgeTenant: opts.Tenant,
} }
token, err := jwtutil.SignedToken(key, signingAlgorithm, claims) token, err := jwt.GenerateSignedToken(algo, key, claims)
if err != nil { if err != nil {
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

View File

@ -5,17 +5,12 @@ import (
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http" edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/util" "forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
const (
CookieName string = "edge-auth"
)
const ( const (
ClaimSubject = "sub" ClaimSubject = "sub"
ClaimIssuer = "iss" ClaimIssuer = "iss"
@ -26,8 +21,8 @@ const (
) )
type Module struct { type Module struct {
server *app.Server server *app.Server
getClaimFn GetClaimFunc getClaims GetClaimsFunc
} }
func (m *Module) Name() string { func (m *Module) Name() string {
@ -73,9 +68,9 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
panic(rt.ToValue(errors.New("could not find http request in context"))) panic(rt.ToValue(errors.New("could not find http request in context")))
} }
claim, err := m.getClaimFn(ctx, req, claimName) claim, err := m.getClaims(ctx, req, claimName)
if err != nil { if err != nil {
if errors.Is(err, jwtutil.ErrUnauthenticated) { if errors.Is(err, ErrUnauthenticated) {
return nil return nil
} }
@ -83,7 +78,11 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
return nil return nil
} }
return rt.ToValue(claim) if len(claim) == 0 || claim[0] == "" {
return nil
}
return rt.ToValue(claim[0])
} }
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory { func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
@ -94,8 +93,8 @@ func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule { return func(server *app.Server) app.ServerModule {
return &Module{ return &Module{
server: server, server: server,
getClaimFn: opt.GetClaim, getClaims: opt.GetClaims,
} }
} }
} }

View File

@ -10,7 +10,6 @@ import (
"cdr.dev/slog" "cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http" edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwk"
@ -131,7 +130,7 @@ func getDummyKey() jwk.Key {
return key return key
} }
func getDummyKeySet(key jwk.Key) jwtutil.GetKeySetFunc { func getDummyKeySet(key jwk.Key) GetKeySetFunc {
return func() (jwk.Set, error) { return func() (jwk.Set, error) {
set := jwk.NewSet() set := jwk.NewSet()

View File

@ -3,7 +3,6 @@ package auth
import ( import (
"net/http" "net/http"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api" "gitlab.com/wpetit/goweb/api"
@ -13,39 +12,39 @@ import (
type MountFunc func(r chi.Router) type MountFunc func(r chi.Router)
type Handler struct { type Handler struct {
getClaim GetClaimFunc getClaims GetClaimsFunc
profileClaims []string profileClaims []string
} }
func (h *Handler) serveProfile(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveProfile(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
profile := make(map[string]any) claims, err := h.getClaims(ctx, r, h.profileClaims...)
if err != nil {
for _, name := range h.profileClaims { if errors.Is(err, ErrUnauthenticated) {
value, err := h.getClaim(ctx, r, name)
if err != nil {
if errors.Is(err, jwtutil.ErrUnauthenticated) {
api.ErrorResponse(
w, http.StatusUnauthorized,
api.ErrCodeUnauthorized,
nil,
)
return
}
logger.Error(ctx, "could not retrieve claims", logger.E(errors.WithStack(err)))
api.ErrorResponse( api.ErrorResponse(
w, http.StatusInternalServerError, w, http.StatusUnauthorized,
api.ErrCodeUnknownError, api.ErrCodeUnauthorized,
nil, nil,
) )
return return
} }
profile[name] = value logger.Error(ctx, "could not retrieve claims", logger.E(errors.WithStack(err)))
api.ErrorResponse(
w, http.StatusInternalServerError,
api.ErrCodeUnknownError,
nil,
)
return
}
profile := make(map[string]any)
for idx, cl := range h.profileClaims {
profile[cl] = claims[idx]
} }
api.DataResponse(w, http.StatusOK, struct { api.DataResponse(w, http.StatusOK, struct {
@ -63,7 +62,7 @@ func Mount(authHandler http.Handler, funcs ...OptionFunc) MountFunc {
handler := &Handler{ handler := &Handler{
profileClaims: opt.ProfileClaims, profileClaims: opt.ProfileClaims,
getClaim: opt.GetClaim, getClaims: opt.GetClaims,
} }
return func(r chi.Router) { return func(r chi.Router) {

View File

@ -2,17 +2,15 @@ package auth
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type GetClaimFunc func(ctx context.Context, r *http.Request, name string) (string, error) type GetClaimsFunc func(ctx context.Context, r *http.Request, claims ...string) ([]string, error)
type Option struct { type Option struct {
GetClaim GetClaimFunc GetClaims GetClaimsFunc
ProfileClaims []string ProfileClaims []string
} }
@ -20,7 +18,7 @@ type OptionFunc func(*Option)
func defaultOptions() *Option { func defaultOptions() *Option {
return &Option{ return &Option{
GetClaim: dummyGetClaim, GetClaims: dummyGetClaims,
ProfileClaims: []string{ ProfileClaims: []string{
ClaimSubject, ClaimSubject,
ClaimIssuer, ClaimIssuer,
@ -32,13 +30,13 @@ func defaultOptions() *Option {
} }
} }
func dummyGetClaim(ctx context.Context, r *http.Request, name string) (string, error) { func dummyGetClaims(ctx context.Context, r *http.Request, claims ...string) ([]string, error) {
return "", errors.Errorf("dummy getclaim func cannot retrieve claim '%s'", name) return nil, errors.Errorf("dummy getclaim func cannot retrieve claims '%s'", claims)
} }
func WithGetClaims(fn GetClaimFunc) OptionFunc { func WithGetClaims(fn GetClaimsFunc) OptionFunc {
return func(o *Option) { return func(o *Option) {
o.GetClaim = fn o.GetClaims = fn
} }
} }
@ -47,34 +45,3 @@ func WithProfileClaims(claims ...string) OptionFunc {
o.ProfileClaims = claims o.ProfileClaims = claims
} }
} }
func WithJWT(getKeySet jwtutil.GetKeySetFunc) OptionFunc {
funcs := []jwtutil.FindTokenOptionFunc{
jwtutil.WithFinders(
jwtutil.FindTokenFromAuthorizationHeader,
jwtutil.FindTokenFromQueryString(CookieName),
jwtutil.FindTokenFromCookie(CookieName),
),
}
return func(o *Option) {
o.GetClaim = func(ctx context.Context, r *http.Request, name string) (string, error) {
token, err := jwtutil.FindToken(r, getKeySet, funcs...)
if err != nil {
return "", errors.WithStack(err)
}
tokenMap, err := token.AsMap(ctx)
if err != nil {
return "", errors.WithStack(err)
}
value, exists := tokenMap[name]
if !exists {
return "", nil
}
return fmt.Sprintf("%v", value), nil
}
}
}

View File

@ -1,14 +1,14 @@
package blob package blob
import ( import (
"os" "io/ioutil"
"testing" "testing"
"cdr.dev/slog" "cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus/memory" "forge.cadoles.com/arcad/edge/pkg/bus/memory"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -27,7 +27,7 @@ func TestBlobModule(t *testing.T) {
ModuleFactory(bus, store), ModuleFactory(bus, store),
) )
data, err := os.ReadFile("testdata/blob.js") data, err := ioutil.ReadFile("testdata/blob.js")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -5,19 +5,18 @@ import (
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/util" "forge.cadoles.com/arcad/edge/pkg/module/util"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const ( const (
AnyType share.ValueType = "*" AnyType ValueType = "*"
AnyName string = "*" AnyName string = "*"
) )
type Module struct { type Module struct {
appID app.ID appID app.ID
store share.Store repository Repository
} }
func (m *Module) Name() string { func (m *Module) Name() string {
@ -49,19 +48,19 @@ func (m *Module) Export(export *goja.Object) {
panic(errors.Wrap(err, "could not set 'ANY_NAME' property")) panic(errors.Wrap(err, "could not set 'ANY_NAME' property"))
} }
if err := export.Set("TYPE_TEXT", share.TypeText); err != nil { if err := export.Set("TYPE_TEXT", TypeText); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property")) panic(errors.Wrap(err, "could not set 'TYPE_TEXT' property"))
} }
if err := export.Set("TYPE_NUMBER", share.TypeNumber); err != nil { if err := export.Set("TYPE_NUMBER", TypeNumber); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property")) panic(errors.Wrap(err, "could not set 'TYPE_NUMBER' property"))
} }
if err := export.Set("TYPE_BOOL", share.TypeBool); err != nil { if err := export.Set("TYPE_BOOL", TypeBool); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property")) panic(errors.Wrap(err, "could not set 'TYPE_BOOL' property"))
} }
if err := export.Set("TYPE_PATH", share.TypePath); err != nil { if err := export.Set("TYPE_PATH", TypePath); err != nil {
panic(errors.Wrap(err, "could not set 'TYPE_PATH' property")) panic(errors.Wrap(err, "could not set 'TYPE_PATH' property"))
} }
} }
@ -70,20 +69,20 @@ func (m *Module) upsertResource(call goja.FunctionCall, rt *goja.Runtime) goja.V
ctx := util.AssertContext(call.Argument(0), rt) ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt) resourceID := assertResourceID(call.Argument(1), rt)
var attributes []share.Attribute var attributes []Attribute
if len(call.Arguments) > 2 { if len(call.Arguments) > 2 {
attributes = assertAttributes(call.Arguments[2:], rt) attributes = assertAttributes(call.Arguments[2:], rt)
} else { } else {
attributes = make([]share.Attribute, 0) attributes = make([]Attribute, 0)
} }
for _, attr := range attributes { for _, attr := range attributes {
if err := share.AssertType(attr.Value(), attr.Type()); err != nil { if err := AssertType(attr.Value(), attr.Type()); err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
} }
resource, err := m.store.UpdateAttributes(ctx, m.appID, resourceID, attributes...) resource, err := m.repository.UpdateAttributes(ctx, m.appID, resourceID, attributes...)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -102,7 +101,7 @@ func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja
names = make([]string, 0) names = make([]string, 0)
} }
err := m.store.DeleteAttributes(ctx, m.appID, resourceID, names...) err := m.repository.DeleteAttributes(ctx, m.appID, resourceID, names...)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -113,23 +112,23 @@ func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja
func (m *Module) findResources(call goja.FunctionCall, rt *goja.Runtime) goja.Value { func (m *Module) findResources(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt) ctx := util.AssertContext(call.Argument(0), rt)
funcs := make([]share.FindResourcesOptionFunc, 0) funcs := make([]FindResourcesOptionFunc, 0)
if len(call.Arguments) > 1 { if len(call.Arguments) > 1 {
name := util.AssertString(call.Argument(1), rt) name := util.AssertString(call.Argument(1), rt)
if name != AnyName { if name != AnyName {
funcs = append(funcs, share.WithName(name)) funcs = append(funcs, WithName(name))
} }
} }
if len(call.Arguments) > 2 { if len(call.Arguments) > 2 {
valueType := assertValueType(call.Argument(2), rt) valueType := assertValueType(call.Argument(2), rt)
if valueType != AnyType { if valueType != AnyType {
funcs = append(funcs, share.WithType(valueType)) funcs = append(funcs, WithType(valueType))
} }
} }
resources, err := m.store.FindResources(ctx, funcs...) resources, err := m.repository.FindResources(ctx, funcs...)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -141,7 +140,7 @@ func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.V
ctx := util.AssertContext(call.Argument(0), rt) ctx := util.AssertContext(call.Argument(0), rt)
resourceID := assertResourceID(call.Argument(1), rt) resourceID := assertResourceID(call.Argument(1), rt)
err := m.store.DeleteResource(ctx, m.appID, resourceID) err := m.repository.DeleteResource(ctx, m.appID, resourceID)
if err != nil { if err != nil {
panic(rt.ToValue(errors.WithStack(err))) panic(rt.ToValue(errors.WithStack(err)))
} }
@ -149,29 +148,29 @@ func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.V
return nil return nil
} }
func ModuleFactory(appID app.ID, store share.Store) app.ServerModuleFactory { func ModuleFactory(appID app.ID, repository Repository) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule { return func(server *app.Server) app.ServerModule {
return &Module{ return &Module{
appID: appID, appID: appID,
store: store, repository: repository,
} }
} }
} }
func assertResourceID(v goja.Value, r *goja.Runtime) share.ResourceID { func assertResourceID(v goja.Value, r *goja.Runtime) ResourceID {
value := v.Export() value := v.Export()
switch typ := value.(type) { switch typ := value.(type) {
case string: case string:
return share.ResourceID(typ) return ResourceID(typ)
case share.ResourceID: case ResourceID:
return typ return typ
default: default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ResourceID, got '%T'", value))) panic(r.ToValue(errors.Errorf("expected value to be a string or ResourceID, got '%T'", value)))
} }
} }
func assertAttributes(values []goja.Value, r *goja.Runtime) []share.Attribute { func assertAttributes(values []goja.Value, r *goja.Runtime) []Attribute {
attributes := make([]share.Attribute, len(values)) attributes := make([]Attribute, len(values))
for idx, val := range values { for idx, val := range values {
export := val.Export() export := val.Export()
@ -196,12 +195,12 @@ func assertAttributes(values []goja.Value, r *goja.Runtime) []share.Attribute {
panic(r.ToValue(errors.Errorf("could not find 'type' property on attribute '%v'", export))) panic(r.ToValue(errors.Errorf("could not find 'type' property on attribute '%v'", export)))
} }
var valueType share.ValueType var valueType ValueType
switch typ := rawType.(type) { switch typ := rawType.(type) {
case share.ValueType: case ValueType:
valueType = typ valueType = typ
case string: case string:
valueType = share.ValueType(typ) valueType = ValueType(typ)
default: default:
panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'type': expected 'string' or 'ValueType', got '%T'", rawType))) panic(r.ToValue(errors.Errorf("unexpected value for attribute property 'type': expected 'string' or 'ValueType', got '%T'", rawType)))
@ -212,7 +211,7 @@ func assertAttributes(values []goja.Value, r *goja.Runtime) []share.Attribute {
panic(r.ToValue(errors.Errorf("could not find 'value' property on attribute '%v'", export))) panic(r.ToValue(errors.Errorf("could not find 'value' property on attribute '%v'", export)))
} }
attributes[idx] = share.NewBaseAttribute( attributes[idx] = NewBaseAttribute(
name, name,
valueType, valueType,
value, value,
@ -233,12 +232,12 @@ func assertStrings(values []goja.Value, r *goja.Runtime) []string {
return strings return strings
} }
func assertValueType(v goja.Value, r *goja.Runtime) share.ValueType { func assertValueType(v goja.Value, r *goja.Runtime) ValueType {
value := v.Export() value := v.Export()
switch typ := value.(type) { switch typ := value.(type) {
case string: case string:
return share.ValueType(typ) return ValueType(typ)
case share.ValueType: case ValueType:
return typ return typ
default: default:
panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value))) panic(r.ToValue(errors.Errorf("expected value to be a string or ValueType, got '%T'", value)))
@ -246,7 +245,7 @@ func assertValueType(v goja.Value, r *goja.Runtime) share.ValueType {
} }
type gojaResource struct { type gojaResource struct {
ID share.ResourceID `goja:"id" json:"id"` ID ResourceID `goja:"id" json:"id"`
Origin app.ID `goja:"origin" json:"origin"` Origin app.ID `goja:"origin" json:"origin"`
Attributes []*gojaAttribute `goja:"attributes" json:"attributes"` Attributes []*gojaAttribute `goja:"attributes" json:"attributes"`
} }
@ -255,7 +254,7 @@ func (r *gojaResource) Has(call goja.FunctionCall, rt *goja.Runtime) goja.Value
name := util.AssertString(call.Argument(0), rt) name := util.AssertString(call.Argument(0), rt)
valueType := assertValueType(call.Argument(1), rt) valueType := assertValueType(call.Argument(1), rt)
hasAttr := share.HasAttribute(toResource(r), name, valueType) hasAttr := HasAttribute(toResource(r), name, valueType)
return rt.ToValue(hasAttr) return rt.ToValue(hasAttr)
} }
@ -269,7 +268,7 @@ func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value
defaultValue = call.Argument(2).Export() defaultValue = call.Argument(2).Export()
} }
attr := share.GetAttribute(toResource(r), name, valueType) attr := GetAttribute(toResource(r), name, valueType)
if attr == nil { if attr == nil {
return rt.ToValue(defaultValue) return rt.ToValue(defaultValue)
@ -279,14 +278,14 @@ func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value
} }
type gojaAttribute struct { type gojaAttribute struct {
Name string `goja:"name" json:"name"` Name string `goja:"name" json:"name"`
Type share.ValueType `goja:"type" json:"type"` Type ValueType `goja:"type" json:"type"`
Value any `goja:"value" json:"value"` Value any `goja:"value" json:"value"`
CreatedAt time.Time `goja:"createdAt" json:"createdAt"` CreatedAt time.Time `goja:"createdAt" json:"createdAt"`
UpdatedAt time.Time `goja:"updatedAt" json:"updatedAt"` UpdatedAt time.Time `goja:"updatedAt" json:"updatedAt"`
} }
func toGojaResource(res share.Resource) *gojaResource { func toGojaResource(res Resource) *gojaResource {
attributes := make([]*gojaAttribute, len(res.Attributes())) attributes := make([]*gojaAttribute, len(res.Attributes()))
for idx, attr := range res.Attributes() { for idx, attr := range res.Attributes() {
@ -306,7 +305,7 @@ func toGojaResource(res share.Resource) *gojaResource {
} }
} }
func toGojaResources(resources []share.Resource) []*gojaResource { func toGojaResources(resources []Resource) []*gojaResource {
gojaResources := make([]*gojaResource, len(resources)) gojaResources := make([]*gojaResource, len(resources))
for idx, res := range resources { for idx, res := range resources {
gojaResources[idx] = toGojaResource(res) gojaResources[idx] = toGojaResource(res)
@ -314,19 +313,19 @@ func toGojaResources(resources []share.Resource) []*gojaResource {
return gojaResources return gojaResources
} }
func toResource(res *gojaResource) share.Resource { func toResource(res *gojaResource) Resource {
return share.NewBaseResource( return NewBaseResource(
res.Origin, res.Origin,
res.ID, res.ID,
toAttributes(res.Attributes)..., toAttributes(res.Attributes)...,
) )
} }
func toAttributes(gojaAttributes []*gojaAttribute) []share.Attribute { func toAttributes(gojaAttributes []*gojaAttribute) []Attribute {
attributes := make([]share.Attribute, len(gojaAttributes)) attributes := make([]Attribute, len(gojaAttributes))
for idx, gojaAttr := range gojaAttributes { for idx, gojaAttr := range gojaAttributes {
attr := share.NewBaseAttribute( attr := NewBaseAttribute(
gojaAttr.Name, gojaAttr.Name,
gojaAttr.Type, gojaAttr.Type,
gojaAttr.Value, gojaAttr.Value,

View File

@ -7,7 +7,7 @@ type FindResourcesOptions struct {
ValueType *ValueType ValueType *ValueType
} }
func NewFindResourcesOptions(funcs ...FindResourcesOptionFunc) *FindResourcesOptions { func FillFindResourcesOptions(funcs ...FindResourcesOptionFunc) *FindResourcesOptions {
opts := &FindResourcesOptions{} opts := &FindResourcesOptions{}
for _, fn := range funcs { for _, fn := range funcs {

View File

@ -23,7 +23,7 @@ type Attribute interface {
CreatedAt() time.Time CreatedAt() time.Time
} }
type Store interface { type Repository interface {
DeleteResource(ctx context.Context, origin app.ID, resourceID ResourceID) error DeleteResource(ctx context.Context, origin app.ID, resourceID ResourceID) error
FindResources(ctx context.Context, funcs ...FindResourcesOptionFunc) ([]Resource, error) FindResources(ctx context.Context, funcs ...FindResourcesOptionFunc) ([]Resource, error)
GetResource(ctx context.Context, origin app.ID, resourceID ResourceID) (Resource, error) GetResource(ctx context.Context, origin app.ID, resourceID ResourceID) (Resource, error)

View File

@ -0,0 +1,13 @@
package sqlite
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/module/share/testsuite"
"gitlab.com/wpetit/goweb/logger"
)
func TestModule(t *testing.T) {
logger.SetLevel(logger.LevelDebug)
testsuite.TestModule(t, newTestRepo)
}

View File

@ -7,18 +7,19 @@ import (
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share" "forge.cadoles.com/arcad/edge/pkg/module/share"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
type ShareStore struct { type Repository struct {
getDB GetDBFunc getDB sqlite.GetDBFunc
} }
// DeleteAttributes implements share.Repository // DeleteAttributes implements share.Repository
func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error { func (r *Repository) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
err := s.withTx(ctx, func(tx *sql.Tx) error { err := r.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
DELETE FROM resources DELETE FROM resources
WHERE origin = $1 AND resource_id = $2 WHERE origin = $1 AND resource_id = $2
@ -75,8 +76,8 @@ func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resour
} }
// DeleteResource implements share.Repository // DeleteResource implements share.Repository
func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error { func (r *Repository) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
err := s.withTx(ctx, func(tx *sql.Tx) error { err := r.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
DELETE FROM resources DELETE FROM resources
WHERE origin = $1 AND resource_id = $2 WHERE origin = $1 AND resource_id = $2
@ -114,12 +115,12 @@ func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resource
} }
// FindResources implements share.Repository // FindResources implements share.Repository
func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) { func (r *Repository) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
opts := share.NewFindResourcesOptions(funcs...) opts := share.FillFindResourcesOptions(funcs...)
var resources []share.Resource var resources []share.Resource
err := s.withTx(ctx, func(tx *sql.Tx) error { err := r.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
SELECT SELECT
main.origin, main.resource_id, main.origin, main.resource_id,
@ -221,14 +222,14 @@ func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResou
} }
// GetResource implements share.Repository // GetResource implements share.Repository
func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) { func (r *Repository) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
var ( var (
resource *share.BaseResource resource *share.BaseResource
err error err error
) )
err = s.withTx(ctx, func(tx *sql.Tx) error { err = r.withTx(ctx, func(tx *sql.Tx) error {
resource, err = s.getResourceWithinTx(ctx, tx, origin, resourceID) resource, err = r.getResourceWithinTx(ctx, tx, origin, resourceID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -243,13 +244,13 @@ func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID
} }
// UpdateAttributes implements share.Repository // UpdateAttributes implements share.Repository
func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) { func (r *Repository) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
if len(attributes) == 0 { if len(attributes) == 0 {
return nil, errors.WithStack(share.ErrAttributeRequired) return nil, errors.WithStack(share.ErrAttributeRequired)
} }
var resource *share.BaseResource var resource *share.BaseResource
err := s.withTx(ctx, func(tx *sql.Tx) error { err := r.withTx(ctx, func(tx *sql.Tx) error {
query := ` query := `
INSERT INTO resources (origin, resource_id, name, type, value, created_at, updated_at) INSERT INTO resources (origin, resource_id, name, type, value, created_at, updated_at)
VALUES($1, $2, $3, $4, $5, $6, $6) VALUES($1, $2, $3, $4, $5, $6, $6)
@ -288,7 +289,7 @@ func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resour
} }
} }
resource, err = s.getResourceWithinTx(ctx, tx, origin, resourceID) resource, err = r.getResourceWithinTx(ctx, tx, origin, resourceID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -302,7 +303,7 @@ func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resour
return resource, nil return resource, nil
} }
func (s *ShareStore) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin app.ID, resourceID share.ResourceID) (*share.BaseResource, error) { func (r *Repository) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin app.ID, resourceID share.ResourceID) (*share.BaseResource, error) {
query := ` query := `
SELECT name, type, value, created_at, updated_at SELECT name, type, value, created_at, updated_at
FROM resources FROM resources
@ -360,23 +361,23 @@ func (s *ShareStore) getResourceWithinTx(ctx context.Context, tx *sql.Tx, origin
return resource, nil return resource, nil
} }
func (s *ShareStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error { func (r *Repository) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
var db *sql.DB var db *sql.DB
db, err := s.getDB(ctx) db, err := r.getDB(ctx)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
if err := WithTx(ctx, db, fn); err != nil { if err := sqlite.WithTx(ctx, db, fn); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil return nil
} }
func ensureShareTables(ctx context.Context, db *sql.DB) error { func ensureTables(ctx context.Context, db *sql.DB) error {
err := WithTx(ctx, db, func(tx *sql.Tx) error { err := sqlite.WithTx(ctx, db, func(tx *sql.Tx) error {
query := ` query := `
CREATE TABLE IF NOT EXISTS resources ( CREATE TABLE IF NOT EXISTS resources (
resource_id TEXT NOT NULL, resource_id TEXT NOT NULL,
@ -409,20 +410,20 @@ func ensureShareTables(ctx context.Context, db *sql.DB) error {
return nil return nil
} }
func NewShareStore(path string) *ShareStore { func NewRepository(path string) *Repository {
getDB := NewGetDBFunc(path, ensureShareTables) getDB := sqlite.NewGetDBFunc(path, ensureTables)
return &ShareStore{ return &Repository{
getDB: getDB, getDB: getDB,
} }
} }
func NewShareStoreWithDB(db *sql.DB) *ShareStore { func NewRepositoryWithDB(db *sql.DB) *Repository {
getDB := NewGetDBFuncFromDB(db, ensureShareTables) getDB := sqlite.NewGetDBFuncFromDB(db, ensureTables)
return &ShareStore{ return &Repository{
getDB: getDB, getDB: getDB,
} }
} }
var _ share.Store = &ShareStore{} var _ share.Repository = &Repository{}

View File

@ -7,18 +7,18 @@ import (
"testing" "testing"
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/storage/share" "forge.cadoles.com/arcad/edge/pkg/module/share"
"forge.cadoles.com/arcad/edge/pkg/storage/share/testsuite" "forge.cadoles.com/arcad/edge/pkg/module/share/testsuite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
func TestRepository(t *testing.T) { func TestRepository(t *testing.T) {
logger.SetLevel(logger.LevelDebug) logger.SetLevel(logger.LevelDebug)
testsuite.TestStore(t, newTestStore) testsuite.TestRepository(t, newTestRepo)
} }
func newTestStore(testName string) (share.Store, error) { func newTestRepo(testName string) (share.Repository, error) {
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_")) filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
file := fmt.Sprintf("./testdata/%s.sqlite", filename) file := fmt.Sprintf("./testdata/%s.sqlite", filename)
@ -27,7 +27,7 @@ func newTestStore(testName string) (share.Store, error) {
} }
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds()) dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := NewShareStore(dsn) repo := NewRepository(dsn)
return store, nil return repo, nil
} }

View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -1,23 +1,21 @@
package share package testsuite
import ( import (
"context" "context"
"os" "io/fs"
"testing" "testing"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage/driver" "forge.cadoles.com/arcad/edge/pkg/module/share"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
) )
func TestModule(t *testing.T) { func TestModule(t *testing.T, newRepo NewTestRepoFunc) {
logger.SetLevel(logger.LevelDebug) logger.SetLevel(logger.LevelDebug)
store, err := driver.NewShareStore("sqlite://testdata/test_share_module.sqlite") repo, err := newRepo("module")
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }
@ -25,10 +23,10 @@ func TestModule(t *testing.T) {
server := app.NewServer( server := app.NewServer(
module.ContextModuleFactory(), module.ContextModuleFactory(),
module.ConsoleModuleFactory(), module.ConsoleModuleFactory(),
ModuleFactory("test.app.edge", store), share.ModuleFactory("test.app.edge", repo),
) )
data, err := os.ReadFile("testdata/share.js") data, err := fs.ReadFile(testData, "testdata/share.js")
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }

View File

@ -0,0 +1,16 @@
package testsuite
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/module/share"
)
type NewTestRepoFunc func(testname string) (share.Repository, error)
func TestRepository(t *testing.T, newRepo NewTestRepoFunc) {
t.Run("Cases", func(t *testing.T) {
t.Parallel()
runRepositoryTests(t, newRepo)
})
}

View File

@ -8,32 +8,32 @@ import (
"time" "time"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share" "forge.cadoles.com/arcad/edge/pkg/module/share"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type repositoryTestCase struct { type repositoryTestCase struct {
Name string Name string
Skip bool Skip bool
Run func(ctx context.Context, t *testing.T, store share.Store) error Run func(ctx context.Context, t *testing.T, repo share.Repository) error
} }
var repositoryTestCases = []repositoryTestCase{ var repositoryTestCases = []repositoryTestCase{
{ {
Name: "Update resource attributes", Name: "Update resource attributes",
Skip: false, Skip: false,
Run: func(ctx context.Context, t *testing.T, store share.Store) error { Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
origin := app.ID("test") origin := app.ID("test")
resourceID := share.ResourceID("test") resourceID := share.ResourceID("test")
// Try to create resource without attributes // Try to create resource without attributes
_, err := store.UpdateAttributes(ctx, origin, resourceID) _, err := repo.UpdateAttributes(ctx, origin, resourceID)
if err == nil { if err == nil {
return errors.New("err should not be nil") return errors.New("err should not be nil")
} }
if !errors.Is(err, share.ErrAttributeRequired) { if !errors.Is(err, share.ErrAttributeRequired) {
return errors.Errorf("err: expected share.ErrAttributeRequired, got '%v'", err) return errors.Errorf("err: expected share.ErrAttributeRequired, got '%+v'", err)
} }
attributes := []share.Attribute{ attributes := []share.Attribute{
@ -43,7 +43,7 @@ var repositoryTestCases = []repositoryTestCase{
share.NewBaseAttribute("my_bool_attr", share.TypeBool, true), share.NewBaseAttribute("my_bool_attr", share.TypeBool, true),
} }
resource, err := store.UpdateAttributes(ctx, origin, resourceID, attributes...) resource, err := repo.UpdateAttributes(ctx, origin, resourceID, attributes...)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -71,12 +71,12 @@ var repositoryTestCases = []repositoryTestCase{
{ {
Name: "Find resources by attribute name", Name: "Find resources by attribute name",
Skip: false, Skip: false,
Run: func(ctx context.Context, t *testing.T, store share.Store) error { Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_name.json", store); err != nil { if err := loadTestData(ctx, "testdata/find_resources_by_attribute_name.json", repo); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
resources, err := store.FindResources(ctx, share.WithName("my_number")) resources, err := repo.FindResources(ctx, share.WithName("my_number"))
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -96,12 +96,12 @@ var repositoryTestCases = []repositoryTestCase{
{ {
Name: "Find resources by attribute type", Name: "Find resources by attribute type",
Skip: false, Skip: false,
Run: func(ctx context.Context, t *testing.T, store share.Store) error { Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type.json", store); err != nil { if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type.json", repo); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
resources, err := store.FindResources(ctx, share.WithType(share.TypePath)) resources, err := repo.FindResources(ctx, share.WithType(share.TypePath))
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -121,12 +121,12 @@ var repositoryTestCases = []repositoryTestCase{
{ {
Name: "Find resources by attribute type and name", Name: "Find resources by attribute type and name",
Skip: false, Skip: false,
Run: func(ctx context.Context, t *testing.T, store share.Store) error { Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type_and_name.json", store); err != nil { if err := loadTestData(ctx, "testdata/find_resources_by_attribute_type_and_name.json", repo); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
resources, err := store.FindResources(ctx, share.WithType(share.TypeText), share.WithName("my_attr")) resources, err := repo.FindResources(ctx, share.WithType(share.TypeText), share.WithName("my_attr"))
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -146,15 +146,15 @@ var repositoryTestCases = []repositoryTestCase{
{ {
Name: "Get resource", Name: "Get resource",
Skip: false, Skip: false,
Run: func(ctx context.Context, t *testing.T, store share.Store) error { Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/get_resource.json", store); err != nil { if err := loadTestData(ctx, "testdata/get_resource.json", repo); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
origin := app.ID("app1.edge.app") origin := app.ID("app1.edge.app")
resourceID := share.ResourceID("res-1") resourceID := share.ResourceID("res-1")
resource, err := store.GetResource(ctx, origin, resourceID) resource, err := repo.GetResource(ctx, origin, resourceID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -172,7 +172,7 @@ var repositoryTestCases = []repositoryTestCase{
return errors.Errorf("resource.ID(): expected '%v', got '%v'", e, g) return errors.Errorf("resource.ID(): expected '%v', got '%v'", e, g)
} }
resource, err = store.GetResource(ctx, origin, "unexistant-id") resource, err = repo.GetResource(ctx, origin, "unexistant-id")
if err == nil { if err == nil {
return errors.New("err should not be nil") return errors.New("err should not be nil")
} }
@ -187,8 +187,8 @@ var repositoryTestCases = []repositoryTestCase{
{ {
Name: "Delete resource", Name: "Delete resource",
Skip: false, Skip: false,
Run: func(ctx context.Context, t *testing.T, store share.Store) error { Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/delete_resource.json", store); err != nil { if err := loadTestData(ctx, "testdata/delete_resource.json", repo); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -196,11 +196,11 @@ var repositoryTestCases = []repositoryTestCase{
resourceID := share.ResourceID("res-1") resourceID := share.ResourceID("res-1")
// It should delete an existing resource // It should delete an existing resource
if err := store.DeleteResource(ctx, origin, resourceID); err != nil { if err := repo.DeleteResource(ctx, origin, resourceID); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
_, err := store.GetResource(ctx, origin, resourceID) _, err := repo.GetResource(ctx, origin, resourceID)
if err == nil { if err == nil {
return errors.New("err should not be nil") return errors.New("err should not be nil")
} }
@ -211,7 +211,7 @@ var repositoryTestCases = []repositoryTestCase{
} }
// It should not delete an unexistant resource // It should not delete an unexistant resource
err = store.DeleteResource(ctx, origin, resourceID) err = repo.DeleteResource(ctx, origin, resourceID)
if err == nil { if err == nil {
return errors.New("err should not be nil") return errors.New("err should not be nil")
} }
@ -223,7 +223,7 @@ var repositoryTestCases = []repositoryTestCase{
otherOrigin := app.ID("app2.edge.app") otherOrigin := app.ID("app2.edge.app")
// It should not delete a resource with the same id and another origin // It should not delete a resource with the same id and another origin
resource, err := store.GetResource(ctx, otherOrigin, resourceID) resource, err := repo.GetResource(ctx, otherOrigin, resourceID)
if err != nil { if err != nil {
return errors.New("err should not be nil") return errors.New("err should not be nil")
} }
@ -238,8 +238,8 @@ var repositoryTestCases = []repositoryTestCase{
{ {
Name: "Delete attributes", Name: "Delete attributes",
Skip: false, Skip: false,
Run: func(ctx context.Context, t *testing.T, store share.Store) error { Run: func(ctx context.Context, t *testing.T, repo share.Repository) error {
if err := loadTestData(ctx, "testdata/delete_attributes.json", store); err != nil { if err := loadTestData(ctx, "testdata/delete_attributes.json", repo); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -247,11 +247,11 @@ var repositoryTestCases = []repositoryTestCase{
resourceID := share.ResourceID("res-1") resourceID := share.ResourceID("res-1")
// It should delete specified attributes // It should delete specified attributes
if err := store.DeleteAttributes(ctx, origin, resourceID, "my_text", "my_bool"); err != nil { if err := repo.DeleteAttributes(ctx, origin, resourceID, "my_text", "my_bool"); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
resource, err := store.GetResource(ctx, origin, resourceID) resource, err := repo.GetResource(ctx, origin, resourceID)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -270,10 +270,12 @@ var repositoryTestCases = []repositoryTestCase{
}, },
} }
func runRepositoryTests(t *testing.T, newRepo NewTestStoreFunc) { func runRepositoryTests(t *testing.T, newRepo NewTestRepoFunc) {
for _, tc := range repositoryTestCases { for _, tc := range repositoryTestCases {
func(tc repositoryTestCase) { func(tc repositoryTestCase) {
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
if tc.Skip { if tc.Skip {
t.SkipNow() t.SkipNow()
@ -309,7 +311,7 @@ type jsonAttribute struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
func loadTestData(ctx context.Context, jsonFile string, store share.Store) error { func loadTestData(ctx context.Context, jsonFile string, repo share.Repository) error {
data, err := testData.ReadFile(jsonFile) data, err := testData.ReadFile(jsonFile)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -332,7 +334,7 @@ func loadTestData(ctx context.Context, jsonFile string, store share.Store) error
) )
} }
_, err := store.UpdateAttributes(ctx, app.ID(res.Origin), share.ResourceID(res.ID), attributes...) _, err := repo.UpdateAttributes(ctx, app.ID(res.Origin), share.ResourceID(res.ID), attributes...)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }

View File

@ -2,12 +2,12 @@ package store
import ( import (
"context" "context"
"os" "io/ioutil"
"testing" "testing"
"forge.cadoles.com/arcad/edge/pkg/app" "forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module" "forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite" "forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -22,7 +22,7 @@ func TestStoreModule(t *testing.T) {
ModuleFactory(store), ModuleFactory(store),
) )
data, err := os.ReadFile("testdata/store.js") data, err := ioutil.ReadFile("testdata/store.js")
if err != nil { if err != nil {
t.Fatalf("%+v", errors.WithStack(err)) t.Fatalf("%+v", errors.WithStack(err))
} }

View File

@ -1,35 +0,0 @@
package driver
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
var blobStoreFactories = make(map[string]BlobStoreFactory, 0)
type BlobStoreFactory func(url *url.URL) (storage.BlobStore, error)
func RegisterBlobStoreFactory(scheme string, factory BlobStoreFactory) {
blobStoreFactories[scheme] = factory
}
func NewBlobStore(dsn string) (storage.BlobStore, error) {
url, err := url.Parse(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
factory, exists := blobStoreFactories[url.Scheme]
if !exists {
return nil, errors.WithStack(ErrSchemeNotRegistered)
}
store, err := factory(url)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}

View File

@ -1,35 +0,0 @@
package driver
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
var documentStoreFactories = make(map[string]DocumentStoreFactory, 0)
type DocumentStoreFactory func(url *url.URL) (storage.DocumentStore, error)
func RegisterDocumentStoreFactory(scheme string, factory DocumentStoreFactory) {
documentStoreFactories[scheme] = factory
}
func NewDocumentStore(dsn string) (storage.DocumentStore, error) {
url, err := url.Parse(dsn)
if err != nil {
return nil, errors.WithStack(err)
}
factory, exists := documentStoreFactories[url.Scheme]
if !exists {
return nil, errors.WithStack(ErrSchemeNotRegistered)
}
store, err := factory(url)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}

View File

@ -1,5 +0,0 @@
package driver
import "errors"
var ErrSchemeNotRegistered = errors.New("scheme was not registered")

View File

@ -1,239 +0,0 @@
package client
import (
"context"
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"github.com/pkg/errors"
)
type BlobBucket struct {
name string
id blob.BucketID
call CallFunc
}
// Size implements storage.BlobBucket
func (b *BlobBucket) Size(ctx context.Context) (int64, error) {
args := blob.GetBucketSizeArgs{
BucketID: b.id,
}
reply := blob.GetBucketSizeReply{}
if err := b.call(ctx, "Service.GetBucketSize", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Size, nil
}
// Name implements storage.BlobBucket
func (b *BlobBucket) Name() string {
return b.name
}
// Close implements storage.BlobBucket
func (b *BlobBucket) Close() error {
args := blob.CloseBucketArgs{
BucketID: b.id,
}
reply := blob.CloseBucketReply{}
if err := b.call(context.Background(), "Service.CloseBucket", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Delete implements storage.BlobBucket
func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error {
args := blob.DeleteBucketArgs{
BucketName: b.name,
}
reply := blob.DeleteBucketReply{}
if err := b.call(context.Background(), "Service.DeleteBucket", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Get implements storage.BlobBucket
func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) {
args := blob.GetBlobInfoArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.GetBlobInfoReply{}
if err := b.call(context.Background(), "Service.GetBlobInfo", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.BlobInfo, nil
}
// List implements storage.BlobBucket
func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) {
args := blob.ListBlobInfoArgs{
BucketID: b.id,
}
reply := blob.ListBlobInfoReply{}
if err := b.call(context.Background(), "Service.ListBlobInfo", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.BlobInfos, nil
}
// NewReader implements storage.BlobBucket
func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadSeekCloser, error) {
args := blob.NewBlobReaderArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.NewBlobReaderReply{}
if err := b.call(context.Background(), "Service.NewBlobReader", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return &blobReaderCloser{
readerID: reply.ReaderID,
call: b.call,
}, nil
}
// NewWriter implements storage.BlobBucket
func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.WriteCloser, error) {
args := blob.NewBlobWriterArgs{
BucketID: b.id,
BlobID: id,
}
reply := blob.NewBlobWriterReply{}
if err := b.call(context.Background(), "Service.NewBlobWriter", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return &blobWriterCloser{
blobID: id,
writerID: reply.WriterID,
call: b.call,
}, nil
}
type blobWriterCloser struct {
blobID storage.BlobID
writerID blob.WriterID
call CallFunc
}
// Write implements io.WriteCloser
func (bwc *blobWriterCloser) Write(data []byte) (int, error) {
args := blob.WriteBlobArgs{
WriterID: bwc.writerID,
Data: data,
}
reply := blob.WriteBlobReply{}
if err := bwc.call(context.Background(), "Service.WriteBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Written, nil
}
// Close implements io.WriteCloser
func (bwc *blobWriterCloser) Close() error {
args := blob.CloseWriterArgs{
WriterID: bwc.writerID,
}
reply := blob.CloseBucketReply{}
if err := bwc.call(context.Background(), "Service.CloseWriter", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
type blobReaderCloser struct {
readerID blob.ReaderID
call func(ctx context.Context, serviceMethod string, args any, reply any) error
}
// Read implements io.ReadSeekCloser
func (brc *blobReaderCloser) Read(p []byte) (int, error) {
args := blob.ReadBlobArgs{
ReaderID: brc.readerID,
Length: len(p),
}
reply := blob.ReadBlobReply{}
if err := brc.call(context.Background(), "Service.ReadBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
copy(p, reply.Data)
if reply.EOF {
return reply.Read, io.EOF
}
return reply.Read, nil
}
// Seek implements io.ReadSeekCloser
func (brc *blobReaderCloser) Seek(offset int64, whence int) (int64, error) {
args := blob.SeekBlobArgs{
ReaderID: brc.readerID,
Offset: offset,
Whence: whence,
}
reply := blob.SeekBlobReply{}
if err := brc.call(context.Background(), "Service.SeekBlob", args, &reply); err != nil {
return 0, errors.WithStack(err)
}
return reply.Read, nil
}
// Close implements io.ReadSeekCloser
func (brc *blobReaderCloser) Close() error {
args := blob.CloseReaderArgs{
ReaderID: brc.readerID,
}
reply := blob.CloseReaderReply{}
if err := brc.call(context.Background(), "Service.CloseReader", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
var (
_ storage.BlobBucket = &BlobBucket{}
_ storage.BlobInfo = &BlobInfo{}
_ io.WriteCloser = &blobWriterCloser{}
_ io.ReadSeekCloser = &blobReaderCloser{}
)

View File

@ -1,40 +0,0 @@
package client
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
type BlobInfo struct {
id storage.BlobID
bucket string
contentType string
modTime time.Time
size int64
}
// Bucket implements storage.BlobInfo
func (i *BlobInfo) Bucket() string {
return i.bucket
}
// ID implements storage.BlobInfo
func (i *BlobInfo) ID() storage.BlobID {
return i.id
}
// ContentType implements storage.BlobInfo
func (i *BlobInfo) ContentType() string {
return i.contentType
}
// ModTime implements storage.BlobInfo
func (i *BlobInfo) ModTime() time.Time {
return i.modTime
}
// Size implements storage.BlobInfo
func (i *BlobInfo) Size() int64 {
return i.size
}

View File

@ -1,101 +0,0 @@
package client
import (
"context"
"net/url"
"github.com/keegancsmith/rpc"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"github.com/pkg/errors"
)
type BlobStore struct {
serverURL *url.URL
}
// DeleteBucket implements storage.BlobStore.
func (s *BlobStore) DeleteBucket(ctx context.Context, name string) error {
args := &blob.DeleteBucketArgs{
BucketName: name,
}
if err := s.call(ctx, "Service.DeleteBucket", args, nil); err != nil {
return errors.WithStack(err)
}
return nil
}
// ListBuckets implements storage.BlobStore.
func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) {
args := &blob.ListBucketsArgs{}
reply := blob.ListBucketsReply{}
if err := s.call(ctx, "Service.ListBuckets", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Buckets, nil
}
// OpenBucket implements storage.BlobStore.
func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBucket, error) {
args := &blob.OpenBucketArgs{
BucketName: name,
}
reply := &blob.OpenBucketReply{}
if err := s.call(ctx, "Service.OpenBucket", args, reply); err != nil {
return nil, errors.WithStack(err)
}
return &BlobBucket{
name: name,
id: reply.BucketID,
call: s.call,
}, nil
}
func (s *BlobStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *BlobStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewBlobStore(serverURL *url.URL) *BlobStore {
return &BlobStore{serverURL}
}
var _ storage.BlobStore = &BlobStore{}

View File

@ -1,87 +0,0 @@
package client
import (
"context"
"fmt"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestBlobStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
httpServer, err := startNewBlobStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewBlobStore(serverURL)
testsuite.TestBlobStore(context.Background(), t, store)
}
func BenchmarkBlobStore(t *testing.B) {
logger.SetLevel(logger.LevelError)
httpServer, err := startNewBlobStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewBlobStore(serverURL)
testsuite.BenchmarkBlobStore(t, store)
}
func getSQLiteBlobStore() (*sqlite.BlobStore, error) {
file := "./testdata/blobstore_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := sqlite.NewBlobStore(dsn)
return store, nil
}
func startNewBlobStoreServer() (*httptest.Server, error) {
store, err := getSQLiteBlobStore()
if err != nil {
return nil, errors.WithStack(err)
}
server := server.NewBlobStoreServer(store)
httpServer := httptest.NewServer(server)
return httpServer, nil
}

View File

@ -1,134 +0,0 @@
package client
import (
"context"
"net/url"
"github.com/keegancsmith/rpc"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/document"
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
)
type DocumentStore struct {
serverURL *url.URL
}
// Delete implements storage.DocumentStore.
func (s *DocumentStore) Delete(ctx context.Context, collection string, id storage.DocumentID) error {
args := document.DeleteDocumentArgs{
Collection: collection,
DocumentID: id,
}
reply := document.DeleteDocumentReply{}
if err := s.call(ctx, "Service.DeleteDocument", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// Get implements storage.DocumentStore.
func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.DocumentID) (storage.Document, error) {
args := document.GetDocumentArgs{
Collection: collection,
DocumentID: id,
}
reply := document.GetDocumentReply{}
if err := s.call(ctx, "Service.GetDocument", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Document, nil
}
// Query implements storage.DocumentStore.
func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) {
opts := &storage.QueryOptions{}
for _, fn := range funcs {
fn(opts)
}
args := document.QueryDocumentsArgs{
Collection: collection,
Filter: nil,
Options: opts,
}
if filter != nil {
args.Filter = filter.AsMap()
}
reply := document.QueryDocumentsReply{
Documents: []storage.Document{},
}
if err := s.call(ctx, "Service.QueryDocuments", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Documents, nil
}
// Upsert implements storage.DocumentStore.
func (s *DocumentStore) Upsert(ctx context.Context, collection string, doc storage.Document) (storage.Document, error) {
args := document.UpsertDocumentArgs{
Collection: collection,
Document: doc,
}
reply := document.UpsertDocumentReply{}
if err := s.call(ctx, "Service.UpsertDocument", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Document, nil
}
func (s *DocumentStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *DocumentStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewDocumentStore(url *url.URL) *DocumentStore {
return &DocumentStore{url}
}
var _ storage.DocumentStore = &DocumentStore{}

View File

@ -1,67 +0,0 @@
package client
import (
"context"
"fmt"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestDocumentStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
httpServer, err := startNewDocumentStoreServer()
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer httpServer.Close()
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
store := NewDocumentStore(serverURL)
testsuite.TestDocumentStore(context.Background(), t, store)
}
func getSQLiteDocumentStore() (*sqlite.DocumentStore, error) {
file := "./testdata/documentstore_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := sqlite.NewDocumentStore(dsn)
return store, nil
}
func startNewDocumentStoreServer() (*httptest.Server, error) {
store, err := getSQLiteDocumentStore()
if err != nil {
return nil, errors.WithStack(err)
}
server := server.NewDocumentStoreServer(store)
httpServer := httptest.NewServer(server)
return httpServer, nil
}

View File

@ -1,17 +0,0 @@
package client
import (
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
func remapShareError(err error) error {
switch errors.Cause(err).Error() {
case share.ErrAttributeRequired.Error():
return share.ErrAttributeRequired
case share.ErrNotFound.Error():
return share.ErrNotFound
default:
return err
}
}

View File

@ -1,9 +0,0 @@
package client
import (
"context"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
)
type CallFunc func(ctx context.Context, serviceMethod string, args any, reply any) error

View File

@ -1,150 +0,0 @@
package client
import (
"context"
"net/url"
"forge.cadoles.com/arcad/edge/pkg/app"
server "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/share"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/keegancsmith/rpc"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type ShareStore struct {
serverURL *url.URL
}
// DeleteAttributes implements share.Store.
func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
args := server.DeleteAttributesArgs{
Origin: origin,
ResourceID: resourceID,
Names: names,
}
reply := server.DeleteAttributesArgs{}
if err := s.call(ctx, "Service.DeleteAttributes", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// DeleteResource implements share.Store.
func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
args := server.DeleteResourceArgs{
Origin: origin,
ResourceID: resourceID,
}
reply := server.DeleteResourceReply{}
if err := s.call(ctx, "Service.DeleteResource", args, &reply); err != nil {
return errors.WithStack(err)
}
return nil
}
// FindResources implements share.Store.
func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
options := share.NewFindResourcesOptions(funcs...)
args := server.FindResourcesArgs{
Options: options,
}
reply := server.FindResourcesReply{}
if err := s.call(ctx, "Service.FindResources", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
resources := make([]share.Resource, len(reply.Resources))
for idx, res := range reply.Resources {
resources[idx] = res
}
return resources, nil
}
// GetResource implements share.Store.
func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
args := server.GetResourceArgs{
Origin: origin,
ResourceID: resourceID,
}
reply := server.GetResourceReply{}
if err := s.call(ctx, "Service.GetResource", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Resource, nil
}
// UpdateAttributes implements share.Store.
func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
serializableAttributes := make([]*server.SerializableAttribute, len(attributes))
for attrIdx, attr := range attributes {
serializableAttributes[attrIdx] = server.FromAttribute(attr)
}
args := server.UpdateAttributesArgs{
Origin: origin,
ResourceID: resourceID,
Attributes: serializableAttributes,
}
reply := server.UpdateAttributesReply{}
if err := s.call(ctx, "Service.UpdateAttributes", args, &reply); err != nil {
return nil, errors.WithStack(err)
}
return reply.Resource, nil
}
func (s *ShareStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
return errors.WithStack(remapShareError(err))
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *ShareStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
}
}()
if err := fn(ctx, client); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewShareStore(url *url.URL) *ShareStore {
return &ShareStore{url}
}
var _ share.Store = &ShareStore{}

View File

@ -1,67 +0,0 @@
package client
import (
"fmt"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"forge.cadoles.com/arcad/edge/pkg/storage/share/testsuite"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestShareStore(t *testing.T) {
t.Parallel()
if testing.Verbose() {
logger.SetLevel(logger.LevelDebug)
}
testsuite.TestStore(t, func(testName string) (share.Store, error) {
httpServer, err := startNewShareStoreServer(testName)
if err != nil {
return nil, errors.WithStack(err)
}
serverAddr := httpServer.Listener.Addr()
serverURL := &url.URL{
Host: serverAddr.String(),
}
return NewShareStore(serverURL), nil
})
}
func getSQLiteShareStore(testName string) (*sqlite.ShareStore, error) {
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
file := fmt.Sprintf("./testdata/sharestore_test_%s.sqlite", filename)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
store := sqlite.NewShareStore(dsn)
return store, nil
}
func startNewShareStoreServer(testName string) (*httptest.Server, error) {
store, err := getSQLiteShareStore(testName)
if err != nil {
return nil, errors.WithStack(err)
}
server := server.NewShareStoreServer(store)
httpServer := httptest.NewServer(server)
return httpServer, nil
}

View File

@ -1,28 +0,0 @@
package rpc
import (
"net/url"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/client"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
func init() {
driver.RegisterDocumentStoreFactory("rpc", documentStoreFactory)
driver.RegisterBlobStoreFactory("rpc", blobStoreFactory)
driver.RegisterShareStoreFactory("rpc", shareStoreFactory)
}
func documentStoreFactory(url *url.URL) (storage.DocumentStore, error) {
return client.NewDocumentStore(url), nil
}
func blobStoreFactory(url *url.URL) (storage.BlobStore, error) {
return client.NewBlobStore(url), nil
}
func shareStoreFactory(url *url.URL) (share.Store, error) {
return client.NewShareStore(url), nil
}

View File

@ -1,42 +0,0 @@
package gob
import (
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
type BlobInfo struct {
Bucket_ string
ContentType_ string
BlobID_ storage.BlobID
ModTime_ time.Time
Size_ int64
}
// Bucket implements storage.BlobInfo.
func (bi *BlobInfo) Bucket() string {
return bi.Bucket_
}
// ContentType implements storage.BlobInfo.
func (bi *BlobInfo) ContentType() string {
return bi.ContentType_
}
// ID implements storage.BlobInfo.
func (bi *BlobInfo) ID() storage.BlobID {
return bi.BlobID_
}
// ModTime implements storage.BlobInfo.
func (bi *BlobInfo) ModTime() time.Time {
return bi.ModTime_
}
// Size implements storage.BlobInfo.
func (bi *BlobInfo) Size() int64 {
return bi.Size_
}
var _ storage.BlobInfo = &BlobInfo{}

View File

@ -1,18 +0,0 @@
package gob
import (
"encoding/gob"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
)
func init() {
gob.Register(storage.Document{})
gob.Register(storage.DocumentID(""))
gob.Register(time.Time{})
gob.Register(map[string]interface{}{})
gob.Register([]interface{}{})
gob.Register([]map[string]interface{}{})
gob.Register(&BlobInfo{})
}

View File

@ -1,31 +0,0 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type CloseBucketArgs struct {
BucketID BucketID
}
type CloseBucketReply struct {
}
func (s *Service) CloseBucket(ctx context.Context, args *CloseBucketArgs, reply *CloseBucketReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
if err := bucket.Close(); err != nil {
return errors.WithStack(err)
}
s.buckets.Delete(args.BucketID)
*reply = CloseBucketReply{}
return nil
}

View File

@ -1,31 +0,0 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type CloseReaderArgs struct {
ReaderID ReaderID
}
type CloseReaderReply struct {
}
func (s *Service) CloseReader(ctx context.Context, args *CloseReaderArgs, reply *CloseReaderReply) error {
reader, err := s.getOpenedReader(args.ReaderID)
if err != nil {
return errors.WithStack(err)
}
if err := reader.Close(); err != nil {
return errors.WithStack(err)
}
s.readers.Delete(args.ReaderID)
*reply = CloseReaderReply{}
return nil
}

View File

@ -1,31 +0,0 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type CloseWriterArgs struct {
WriterID WriterID
}
type CloseWriterReply struct {
}
func (s *Service) CloseWriter(ctx context.Context, args *CloseWriterArgs, reply *CloseWriterReply) error {
writer, err := s.getOpenedWriter(args.WriterID)
if err != nil {
return errors.WithStack(err)
}
if err := writer.Close(); err != nil {
return errors.WithStack(err)
}
s.writers.Delete(args.WriterID)
*reply = CloseWriterReply{}
return nil
}

View File

@ -1,22 +0,0 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type DeleteBucketArgs struct {
BucketName string
}
type DeleteBucketReply struct {
}
func (s *Service) DeleteBucket(ctx context.Context, args *DeleteBucketArgs, reply *DeleteBucketReply) error {
if err := s.store.DeleteBucket(ctx, args.BucketName); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@ -1,42 +0,0 @@
package blob
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
"github.com/pkg/errors"
)
type GetBlobInfoArgs struct {
BlobID storage.BlobID
BucketID BucketID
}
type GetBlobInfoReply struct {
BlobInfo storage.BlobInfo
}
func (s *Service) GetBlobInfo(ctx context.Context, args *GetBlobInfoArgs, reply *GetBlobInfoReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
blobInfo, err := bucket.Get(ctx, args.BlobID)
if err != nil {
return errors.WithStack(err)
}
*reply = GetBlobInfoReply{
BlobInfo: &gob.BlobInfo{
Bucket_: blobInfo.Bucket(),
ContentType_: blobInfo.ContentType(),
BlobID_: blobInfo.ID(),
ModTime_: blobInfo.ModTime(),
Size_: blobInfo.Size(),
},
}
return nil
}

View File

@ -1,33 +0,0 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type GetBucketSizeArgs struct {
BucketID BucketID
}
type GetBucketSizeReply struct {
Size int64
}
func (s *Service) GetBucketSize(ctx context.Context, args *GetBucketSizeArgs, reply *GetBucketSizeReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
size, err := bucket.Size(ctx)
if err != nil {
return errors.WithStack(err)
}
*reply = GetBucketSizeReply{
Size: size,
}
return nil
}

View File

@ -1,34 +0,0 @@
package blob
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type ListBlobInfoArgs struct {
BucketID BucketID
}
type ListBlobInfoReply struct {
BlobInfos []storage.BlobInfo
}
func (s *Service) ListBlobInfo(ctx context.Context, args *ListBlobInfoArgs, reply *ListBlobInfoReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
blobInfos, err := bucket.List(ctx)
if err != nil {
return errors.WithStack(err)
}
*reply = ListBlobInfoReply{
BlobInfos: blobInfos,
}
return nil
}

View File

@ -1,27 +0,0 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type ListBucketsArgs struct {
}
type ListBucketsReply struct {
Buckets []string
}
func (s *Service) ListBuckets(ctx context.Context, args *ListBucketsArgs, reply *ListBucketsReply) error {
buckets, err := s.store.ListBuckets(ctx)
if err != nil {
return errors.WithStack(err)
}
*reply = ListBucketsReply{
Buckets: buckets,
}
return nil
}

View File

@ -1,57 +0,0 @@
package blob
import (
"context"
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type NewBlobReaderArgs struct {
BlobID storage.BlobID
BucketID BucketID
}
type NewBlobReaderReply struct {
ReaderID ReaderID
}
func (s *Service) NewBlobReader(ctx context.Context, args *NewBlobReaderArgs, reply *NewBlobReaderReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
readerID, err := NewReaderID()
if err != nil {
return errors.WithStack(err)
}
reader, err := bucket.NewReader(ctx, args.BlobID)
if err != nil {
return errors.WithStack(err)
}
s.readers.Store(readerID, reader)
*reply = NewBlobReaderReply{
ReaderID: readerID,
}
return nil
}
func (s *Service) getOpenedReader(id ReaderID) (io.ReadSeekCloser, error) {
raw, exists := s.readers.Load(id)
if !exists {
return nil, errors.Errorf("could not find writer '%s'", id)
}
reader, ok := raw.(io.ReadSeekCloser)
if !ok {
return nil, errors.Errorf("unexpected type '%T' for writer", raw)
}
return reader, nil
}

View File

@ -1,57 +0,0 @@
package blob
import (
"context"
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type NewBlobWriterArgs struct {
BlobID storage.BlobID
BucketID BucketID
}
type NewBlobWriterReply struct {
WriterID WriterID
}
func (s *Service) NewBlobWriter(ctx context.Context, args *NewBlobWriterArgs, reply *NewBlobWriterReply) error {
bucket, err := s.getOpenedBucket(args.BucketID)
if err != nil {
return errors.WithStack(err)
}
writerID, err := NewWriterID()
if err != nil {
return errors.WithStack(err)
}
writer, err := bucket.NewWriter(ctx, args.BlobID)
if err != nil {
return errors.WithStack(err)
}
s.writers.Store(writerID, writer)
*reply = NewBlobWriterReply{
WriterID: writerID,
}
return nil
}
func (s *Service) getOpenedWriter(id WriterID) (io.WriteCloser, error) {
raw, exists := s.writers.Load(id)
if !exists {
return nil, errors.Errorf("could not find writer '%s'", id)
}
writer, ok := raw.(io.WriteCloser)
if !ok {
return nil, errors.Errorf("unexpected type '%T' for writer", raw)
}
return writer, nil
}

View File

@ -1,50 +0,0 @@
package blob
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type OpenBucketArgs struct {
BucketName string
}
type OpenBucketReply struct {
BucketID BucketID
}
func (s *Service) OpenBucket(ctx context.Context, args *OpenBucketArgs, reply *OpenBucketReply) error {
bucket, err := s.store.OpenBucket(ctx, args.BucketName)
if err != nil {
return errors.WithStack(err)
}
bucketID, err := NewBucketID()
if err != nil {
return errors.WithStack(err)
}
s.buckets.Store(bucketID, bucket)
*reply = OpenBucketReply{
BucketID: bucketID,
}
return nil
}
func (s *Service) getOpenedBucket(id BucketID) (storage.BlobBucket, error) {
raw, exists := s.buckets.Load(id)
if !exists {
return nil, errors.WithStack(storage.ErrBucketClosed)
}
bucket, ok := raw.(storage.BlobBucket)
if !ok {
return nil, errors.Errorf("unexpected type '%T' for blob bucket", raw)
}
return bucket, nil
}

View File

@ -1,41 +0,0 @@
package blob
import (
"context"
"io"
"github.com/pkg/errors"
)
type ReadBlobArgs struct {
ReaderID ReaderID
Length int
}
type ReadBlobReply struct {
Data []byte
Read int
EOF bool
}
func (s *Service) ReadBlob(ctx context.Context, args *ReadBlobArgs, reply *ReadBlobReply) error {
reader, err := s.getOpenedReader(args.ReaderID)
if err != nil {
return errors.WithStack(err)
}
buff := make([]byte, args.Length)
read, err := reader.Read(buff)
if err != nil && !errors.Is(err, io.EOF) {
return errors.WithStack(err)
}
*reply = ReadBlobReply{
Read: read,
Data: buff,
EOF: errors.Is(err, io.EOF),
}
return nil
}

View File

@ -1,38 +0,0 @@
package blob
import (
"context"
"io"
"github.com/pkg/errors"
)
type SeekBlobArgs struct {
ReaderID ReaderID
Offset int64
Whence int
}
type SeekBlobReply struct {
Read int64
EOF bool
}
func (s *Service) SeekBlob(ctx context.Context, args *SeekBlobArgs, reply *SeekBlobReply) error {
reader, err := s.getOpenedReader(args.ReaderID)
if err != nil {
return errors.WithStack(err)
}
read, err := reader.Seek(args.Offset, args.Whence)
if err != nil && !errors.Is(err, io.EOF) {
return errors.WithStack(err)
}
*reply = SeekBlobReply{
Read: read,
EOF: errors.Is(err, io.EOF),
}
return nil
}

View File

@ -1,60 +0,0 @@
package blob
import (
"fmt"
"sync"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/google/uuid"
"github.com/pkg/errors"
)
type BucketID string
type WriterID string
type ReaderID string
type Service struct {
store storage.BlobStore
buckets sync.Map
writers sync.Map
readers sync.Map
}
func NewService(store storage.BlobStore) *Service {
return &Service{
store: store,
}
}
func NewBucketID() (BucketID, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.WithStack(err)
}
id := BucketID(fmt.Sprintf("bucket-%s", uuid.String()))
return id, nil
}
func NewWriterID() (WriterID, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.WithStack(err)
}
id := WriterID(fmt.Sprintf("writer-%s", uuid.String()))
return id, nil
}
func NewReaderID() (ReaderID, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.WithStack(err)
}
id := ReaderID(fmt.Sprintf("reader-%s", uuid.String()))
return id, nil
}

View File

@ -1,34 +0,0 @@
package blob
import (
"context"
"github.com/pkg/errors"
)
type WriteBlobArgs struct {
WriterID WriterID
Data []byte
}
type WriteBlobReply struct {
Written int
}
func (s *Service) WriteBlob(ctx context.Context, args *WriteBlobArgs, reply *WriteBlobReply) error {
writer, err := s.getOpenedWriter(args.WriterID)
if err != nil {
return errors.WithStack(err)
}
written, err := writer.Write(args.Data)
if err != nil {
return errors.WithStack(err)
}
*reply = WriteBlobReply{
Written: written,
}
return nil
}

View File

@ -1,26 +0,0 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type DeleteDocumentArgs struct {
Collection string
DocumentID storage.DocumentID
}
type DeleteDocumentReply struct {
}
func (s *Service) DeleteDocument(ctx context.Context, args DeleteDocumentArgs, reply *DeleteDocumentReply) error {
if err := s.store.Delete(ctx, args.Collection, args.DocumentID); err != nil {
return errors.WithStack(err)
}
*reply = DeleteDocumentReply{}
return nil
}

View File

@ -1,30 +0,0 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type GetDocumentArgs struct {
Collection string
DocumentID storage.DocumentID
}
type GetDocumentReply struct {
Document storage.Document
}
func (s *Service) GetDocument(ctx context.Context, args GetDocumentArgs, reply *GetDocumentReply) error {
document, err := s.store.Get(ctx, args.Collection, args.DocumentID)
if err != nil {
return errors.WithStack(err)
}
*reply = GetDocumentReply{
Document: document,
}
return nil
}

View File

@ -1,53 +0,0 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
"github.com/pkg/errors"
)
type QueryDocumentsArgs struct {
Collection string
Filter map[string]any
Options *storage.QueryOptions
}
type QueryDocumentsReply struct {
Documents []storage.Document
}
func (s *Service) QueryDocuments(ctx context.Context, args QueryDocumentsArgs, reply *QueryDocumentsReply) error {
var (
argsFilter *filter.Filter
err error
)
if args.Filter != nil {
argsFilter, err = filter.NewFrom(args.Filter)
if err != nil {
return errors.WithStack(err)
}
}
documents, err := s.store.Query(ctx, args.Collection, argsFilter, withQueryOptions(args.Options))
if err != nil {
return errors.WithStack(err)
}
*reply = QueryDocumentsReply{
Documents: documents,
}
return nil
}
func withQueryOptions(opts *storage.QueryOptions) storage.QueryOptionFunc {
return func(o *storage.QueryOptions) {
o.Limit = opts.Limit
o.Offset = opts.Offset
o.OrderBy = opts.OrderBy
o.OrderDirection = opts.OrderDirection
}
}

View File

@ -1,11 +0,0 @@
package document
import "forge.cadoles.com/arcad/edge/pkg/storage"
type Service struct {
store storage.DocumentStore
}
func NewService(store storage.DocumentStore) *Service {
return &Service{store}
}

View File

@ -1,30 +0,0 @@
package document
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/pkg/errors"
)
type UpsertDocumentArgs struct {
Collection string
Document storage.Document
}
type UpsertDocumentReply struct {
Document storage.Document
}
func (s *Service) UpsertDocument(ctx context.Context, args UpsertDocumentArgs, reply *UpsertDocumentReply) error {
document, err := s.store.Upsert(ctx, args.Collection, args.Document)
if err != nil {
return errors.WithStack(err)
}
*reply = UpsertDocumentReply{
Document: document,
}
return nil
}

View File

@ -1,5 +0,0 @@
package server
import (
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
)

View File

@ -1,29 +0,0 @@
package server
import (
"github.com/keegancsmith/rpc"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/blob"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/document"
shareService "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/share"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
func NewBlobStoreServer(store storage.BlobStore) *rpc.Server {
server := rpc.NewServer()
server.Register(blob.NewService(store))
return server
}
func NewDocumentStoreServer(store storage.DocumentStore) *rpc.Server {
server := rpc.NewServer()
server.Register(document.NewService(store))
return server
}
func NewShareStoreServer(store share.Store) *rpc.Server {
server := rpc.NewServer()
server.Register(shareService.NewService(store))
return server
}

View File

@ -1,28 +0,0 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type DeleteAttributesArgs struct {
Origin app.ID
ResourceID share.ResourceID
Names []string
}
type DeleteAttributesReply struct {
}
func (s *Service) DeleteAttributes(ctx context.Context, args DeleteAttributesArgs, reply *DeleteAttributesReply) error {
if err := s.store.DeleteAttributes(ctx, args.Origin, args.ResourceID, args.Names...); err != nil {
return errors.WithStack(err)
}
*reply = DeleteAttributesReply{}
return nil
}

View File

@ -1,27 +0,0 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type DeleteResourceArgs struct {
Origin app.ID
ResourceID share.ResourceID
}
type DeleteResourceReply struct {
}
func (s *Service) DeleteResource(ctx context.Context, args DeleteResourceArgs, reply *DeleteResourceReply) error {
if err := s.store.DeleteResource(ctx, args.Origin, args.ResourceID); err != nil {
return errors.WithStack(err)
}
*reply = DeleteResourceReply{}
return nil
}

View File

@ -1,41 +0,0 @@
package share
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
"github.com/pkg/errors"
)
type FindResourcesArgs struct {
Options *share.FindResourcesOptions
}
type FindResourcesReply struct {
Resources []*SerializableResource
}
func (s *Service) FindResources(ctx context.Context, args FindResourcesArgs, reply *FindResourcesReply) error {
resources, err := s.store.FindResources(ctx, withFindResourcesOptions(args.Options))
if err != nil {
return errors.WithStack(err)
}
serializableResources := make([]*SerializableResource, len(resources))
for resIdx, r := range resources {
serializableResources[resIdx] = FromResource(r)
}
*reply = FindResourcesReply{
Resources: serializableResources,
}
return nil
}
func withFindResourcesOptions(opts *share.FindResourcesOptions) share.FindResourcesOptionFunc {
return func(o *share.FindResourcesOptions) {
o.Name = opts.Name
o.ValueType = opts.ValueType
}
}

Some files were not shown because too many files have changed in this diff Show More