Compare commits
1 Commits
7a703f30cc
...
f3c5eee8c8
Author | SHA1 | Date | |
---|---|---|---|
f3c5eee8c8 |
@ -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
6
.gitignore
vendored
@ -5,9 +5,3 @@
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
/.gitea-release
|
/.gitea-release
|
||||||
/.edge
|
/.edge
|
||||||
/data
|
|
||||||
.mktools/
|
|
||||||
/dist
|
|
||||||
/.chglog
|
|
||||||
/CHANGELOG.md
|
|
||||||
/storage-server.key
|
|
117
.goreleaser.yml
117
.goreleaser.yml
@ -1,117 +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
|
|
||||||
scripts:
|
|
||||||
postinstall: "misc/packaging/common/postinstall-storage-server.sh"
|
|
3
Jenkinsfile
vendored
3
Jenkinsfile
vendored
@ -34,8 +34,7 @@ pipeline {
|
|||||||
passwordVariable: 'GITEA_RELEASE_PASSWORD'
|
passwordVariable: 'GITEA_RELEASE_PASSWORD'
|
||||||
])
|
])
|
||||||
]) {
|
]) {
|
||||||
sh 'make .mktools'
|
sh 'make gitea-release'
|
||||||
sh "export MKT_PROJECT_VERSION_BRANCH_NAME=${env.BRANCH_NAME}; make gitea-release"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
59
Makefile
59
Makefile
@ -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: .mktools 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: .env .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
|
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -16,19 +17,19 @@ 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"
|
||||||
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
|
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||||
authModuleMiddleware "forge.cadoles.com/arcad/edge/pkg/module/auth/middleware"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||||
"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 +44,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 +74,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 +95,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 +143,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 +156,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 +181,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,19 +208,13 @@ 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(
|
|
||||||
authModuleMiddleware.AnonymousUser(key, jwa.HS256),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if err := handler.Load(bundle); err != nil {
|
if err := handler.Load(bundle); err != nil {
|
||||||
return errors.Wrap(err, "could not load app bundle")
|
return errors.Wrap(err, "could not load app bundle")
|
||||||
@ -264,7 +242,7 @@ type moduleDeps struct {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,16 +259,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 +317,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 +418,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 +431,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 +465,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"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 CheckToken() *cli.Command {
|
|
||||||
return &cli.Command{
|
|
||||||
Name: "check-token",
|
|
||||||
Usage: "Validate and print the given token with the private key",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "token",
|
|
||||||
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)
|
|
||||||
rawToken := ctx.String("token")
|
|
||||||
|
|
||||||
if rawToken == "" {
|
|
||||||
return errors.New("you must provide a value for --token flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey, err := jwtutil.LoadOrGenerateKey(
|
|
||||||
privateKeyFile,
|
|
||||||
privateKeyDefaultSize,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keySet, err := jwtutil.NewKeySet()
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = jwtutil.AddKeyWithSigningAlgo(keySet, privateKey, jwa.SignatureAlgorithm(signingAlgorithm))
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := jwtutil.Parse([]byte(rawToken), keySet)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, err := token.AsMap(ctx.Context)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
json, err := json.MarshalIndent(claims, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(string(json))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +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(),
|
|
||||||
CheckToken(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,293 +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.IntFlag{
|
|
||||||
Name: "log-level",
|
|
||||||
EnvVars: []string{"STORAGE_SERVER_LOG_LEVEL"},
|
|
||||||
Value: int(logger.LevelError),
|
|
||||||
},
|
|
||||||
&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)
|
|
||||||
logLevel := ctx.Int("log-level")
|
|
||||||
|
|
||||||
logger.SetLevel(logger.Level(logLevel))
|
|
||||||
|
|
||||||
router := chi.NewRouter()
|
|
||||||
|
|
||||||
privateKey, err := jwtutil.LoadOrGenerateKey(
|
|
||||||
privateKeyFile,
|
|
||||||
privateKeyDefaultSize,
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
logger.Debug(ctx.Context, "using authentication", logger.F("privateKey", privateKeyFile), logger.F("signingAlgorithm", signingAlgorithm))
|
|
||||||
|
|
||||||
router.Use(authenticate(privateKey, 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))
|
|
||||||
|
|
||||||
logger.Info(ctx.Context, "listening", logger.F("addr", addr))
|
|
||||||
|
|
||||||
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() {
|
|
||||||
var keySet jwk.Set
|
|
||||||
|
|
||||||
keySet, err = jwtutil.NewKeySet()
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = jwtutil.AddKeyWithSigningAlgo(keySet, privateKey, signingAlgorithm)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,14 +2,6 @@
|
|||||||
|
|
||||||
Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs.
|
Ce module permet de récupérer des informations concernant l'utilisateur connecté et ses attributs.
|
||||||
|
|
||||||
### Utilisateurs anonymes
|
|
||||||
|
|
||||||
Edge génère automatiquement une session pour les utilisateurs anonymes. Ainsi, qu'un utilisateur soit identifié ou non les `claims` suivants seront toujours valués:
|
|
||||||
|
|
||||||
- `auth.CLAIM_SUBJECT`
|
|
||||||
- `auth.CLAIM_PREFERRED_USERNAME`
|
|
||||||
- `auth.CLAIM_ISSUER` (prendra la valeur `anon` dans le cas d'un utilisateur anonyme)
|
|
||||||
|
|
||||||
## Méthodes
|
## Méthodes
|
||||||
|
|
||||||
### `auth.getClaim(ctx: Context, name: string): string`
|
### `auth.getClaim(ctx: Context, name: string): string`
|
||||||
|
@ -43,6 +43,12 @@ function onClientMessage(ctx, message) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Propriétés
|
||||||
|
|
||||||
|
### `context.SESSION_ID`
|
||||||
|
|
||||||
|
Clé permettant de récupérer la clé de session associé au client émetteur du message courant.
|
||||||
|
|
||||||
#### Usage
|
#### Usage
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
7
go.mod
7
go.mod
@ -1,11 +1,10 @@
|
|||||||
module forge.cadoles.com/arcad/edge
|
module forge.cadoles.com/arcad/edge
|
||||||
|
|
||||||
go 1.21
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
github.com/hashicorp/golang-lru/v2 v2.0.3
|
||||||
github.com/hashicorp/mdns v1.0.5
|
github.com/hashicorp/mdns v1.0.5
|
||||||
github.com/keegancsmith/rpc v1.3.0
|
|
||||||
github.com/klauspost/compress v1.16.6
|
github.com/klauspost/compress v1.16.6
|
||||||
github.com/lestrrat-go/jwx/v2 v2.0.8
|
github.com/lestrrat-go/jwx/v2 v2.0.8
|
||||||
github.com/ulikunitz/xz v0.5.11
|
github.com/ulikunitz/xz v0.5.11
|
||||||
@ -47,7 +46,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
|
||||||
|
6
go.sum
6
go.sum
@ -188,8 +188,8 @@ 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.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.3/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 +203,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/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
@ -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>
|
@ -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)
|
|
||||||
});
|
|
||||||
});
|
|
@ -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 || {}));
|
|
@ -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);
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -1,11 +0,0 @@
|
|||||||
#!/sbin/openrc-run
|
|
||||||
|
|
||||||
command="/usr/bin/storage-server"
|
|
||||||
command_args="run"
|
|
||||||
supervisor=supervise-daemon
|
|
||||||
output_log="/var/log/storage-server.log"
|
|
||||||
error_log="$output_log"
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
need net
|
|
||||||
}
|
|
@ -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
|
|
@ -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 run
|
|
||||||
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
|
|
13
modd.conf
13
modd.conf
@ -2,18 +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
|
|
||||||
modd.conf
|
|
||||||
.env
|
|
||||||
{
|
|
||||||
prep: make build-sdk build-cli build-storage-server
|
|
||||||
daemon: make run-app
|
|
||||||
daemon: make run-storage-server
|
|
||||||
}
|
|
||||||
|
|
||||||
misc/client-sdk-testsuite/src/**/*
|
misc/client-sdk-testsuite/src/**/*
|
||||||
|
modd.conf
|
||||||
{
|
{
|
||||||
|
prep: make build-sdk
|
||||||
prep: make build-client-sdk-test-app
|
prep: make build-client-sdk-test-app
|
||||||
|
prep: make build
|
||||||
|
daemon: make run-app
|
||||||
}
|
}
|
||||||
|
|
||||||
**/*.go {
|
**/*.go {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
)
|
|
@ -1,153 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testCases = []func(t *testing.T, z *ZimReader){
|
|
||||||
testOpen,
|
|
||||||
testData,
|
|
||||||
testDisplayArticle,
|
|
||||||
testDisplayInfost,
|
|
||||||
testFavicon,
|
|
||||||
testListArticles,
|
|
||||||
testMainPage,
|
|
||||||
testMetadata,
|
|
||||||
testMime,
|
|
||||||
testURLAtIdx,
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZim(t *testing.T) {
|
|
||||||
zimFiles, err := filepath.Glob("testdata/*.zim")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, zf := range zimFiles {
|
|
||||||
zr, err := NewReader(zf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
base := filepath.Base(zf)
|
|
||||||
|
|
||||||
t.Run(base, func(t *testing.T) {
|
|
||||||
for _, fn := range testCases {
|
|
||||||
testName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
|
|
||||||
t.Run(testName, func(t *testing.T) {
|
|
||||||
fn(t, zr)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testOpen(t *testing.T, zr *ZimReader) {
|
|
||||||
if zr.ArticleCount == 0 {
|
|
||||||
t.Errorf("No article found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMime(t *testing.T, zr *ZimReader) {
|
|
||||||
if len(zr.MimeTypes()) == 0 {
|
|
||||||
t.Errorf("No mime types found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDisplayInfost(t *testing.T, zr *ZimReader) {
|
|
||||||
info := zr.String()
|
|
||||||
if len(info) < 0 {
|
|
||||||
t.Errorf("Can't read infos")
|
|
||||||
}
|
|
||||||
t.Log(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testURLAtIdx(t *testing.T, zr *ZimReader) {
|
|
||||||
// addr 0 is a redirect
|
|
||||||
p, _ := zr.OffsetAtURLIdx(5)
|
|
||||||
a, _ := zr.ArticleAt(p)
|
|
||||||
if a == nil {
|
|
||||||
t.Errorf("Can't find 1st url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDisplayArticle(t *testing.T, zr *ZimReader) {
|
|
||||||
// addr 0 is a redirect
|
|
||||||
p, _ := zr.OffsetAtURLIdx(5)
|
|
||||||
a, _ := zr.ArticleAt(p)
|
|
||||||
if a == nil {
|
|
||||||
t.Errorf("Can't find 1st url")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testListArticles(t *testing.T, zr *ZimReader) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skipping test in short mode.")
|
|
||||||
}
|
|
||||||
|
|
||||||
var i uint32
|
|
||||||
|
|
||||||
for a := range zr.ListArticles() {
|
|
||||||
i++
|
|
||||||
t.Log(a.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == 0 {
|
|
||||||
t.Errorf("Can't find any urls")
|
|
||||||
}
|
|
||||||
|
|
||||||
if i != zr.ArticleCount-1 {
|
|
||||||
t.Errorf("Can't find the exact ArticleCount urls %d vs %d", i, zr.ArticleCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMainPage(t *testing.T, zr *ZimReader) {
|
|
||||||
a, _ := zr.MainPage()
|
|
||||||
if a == nil {
|
|
||||||
t.Errorf("Can't find the mainpage article")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testFavicon(t *testing.T, zr *ZimReader) {
|
|
||||||
favicon, err := zr.Favicon()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
if favicon == nil {
|
|
||||||
t.Errorf("Can't find the favicon article")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMetadata(t *testing.T, zr *ZimReader) {
|
|
||||||
metadata, err := zr.Metadata()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
if metadata == nil {
|
|
||||||
t.Errorf("Can't find the metadata")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testData(t *testing.T, zr *ZimReader) {
|
|
||||||
// addr 0 is a redirect
|
|
||||||
p, _ := zr.OffsetAtURLIdx(2)
|
|
||||||
a, _ := zr.ArticleAt(p)
|
|
||||||
b, _ := a.Data()
|
|
||||||
data := string(b)
|
|
||||||
if a.EntryType != RedirectEntry {
|
|
||||||
if len(data) == 0 {
|
|
||||||
t.Error("can't read data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Log(a.String())
|
|
||||||
t.Log(data)
|
|
||||||
}
|
|
@ -1,233 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type zimCompression uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
zimCompressionNoneZeno zimCompression = 0
|
|
||||||
zimCompressionNone zimCompression = 1
|
|
||||||
zimCompressionNoneZLib zimCompression = 2
|
|
||||||
zimCompressionNoneBZip2 zimCompression = 3
|
|
||||||
zimCompressionNoneXZ zimCompression = 4
|
|
||||||
zimCompressionNoneZStandard zimCompression = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContentEntry struct {
|
|
||||||
*BaseEntry
|
|
||||||
mimeType string
|
|
||||||
clusterIndex uint32
|
|
||||||
blobIndex uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ContentEntry) Reader() (io.Reader, error) {
|
|
||||||
data := make([]byte, 8)
|
|
||||||
|
|
||||||
startClusterPtrOffset := e.reader.clusterPtrPos + (uint64(e.clusterIndex) * 8)
|
|
||||||
if err := e.reader.readRange(int64(startClusterPtrOffset), data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
startClusterOffset, err := readUint64(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endClusterPtrOffset := e.reader.clusterPtrPos + (uint64(e.clusterIndex+1) * 8)
|
|
||||||
if err := e.reader.readRange(int64(endClusterPtrOffset), data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endClusterOffset, err := readUint64(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data = make([]byte, 1)
|
|
||||||
if err := e.reader.readRange(int64(startClusterPtrOffset), data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterHeader := uint8(data[0])
|
|
||||||
|
|
||||||
compression := (clusterHeader << 4) >> 4
|
|
||||||
extended := (clusterHeader<<3)>>7 == 1
|
|
||||||
|
|
||||||
log.Printf("%08b %v %04b %d %d %d", clusterHeader, extended, compression, compression, startClusterOffset, endClusterOffset)
|
|
||||||
|
|
||||||
switch compression {
|
|
||||||
case uint8(zimCompressionNoneZeno):
|
|
||||||
fallthrough
|
|
||||||
case uint8(zimCompressionNone):
|
|
||||||
|
|
||||||
case uint8(zimCompressionNoneXZ):
|
|
||||||
|
|
||||||
case uint8(zimCompressionNoneZStandard):
|
|
||||||
|
|
||||||
case uint8(zimCompressionNoneZLib):
|
|
||||||
fallthrough
|
|
||||||
case uint8(zimCompressionNoneBZip2):
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
// return nil, errors.Wrapf(ErrCompressionAlgorithmNotSupported, "unexpected compression algorithm '%d'", compression)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var internal []byte
|
|
||||||
buff := bytes.NewBuffer(internal)
|
|
||||||
|
|
||||||
// blob starts at offset, blob ends at offset
|
|
||||||
// var bs, be uint32
|
|
||||||
|
|
||||||
// // LZMA: 4, Zstandard: 5
|
|
||||||
// if compression == 4 || compression == 5 {
|
|
||||||
// var blob []byte
|
|
||||||
// var ok bool
|
|
||||||
// var dec io.ReadCloser
|
|
||||||
// if blob, ok = blobLookup(); !ok {
|
|
||||||
// b, err := a.z.bytesRangeAt(start+1, end+1)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// bbuf := bytes.NewBuffer(b)
|
|
||||||
// switch compression {
|
|
||||||
// case 5:
|
|
||||||
// dec, err = NewZstdReader(bbuf)
|
|
||||||
|
|
||||||
// case 4:
|
|
||||||
// dec, err = NewXZReader(bbuf)
|
|
||||||
// }
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// defer dec.Close()
|
|
||||||
// // the decoded chunk are around 1MB
|
|
||||||
// b, err = ioutil.ReadAll(dec)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// blob = make([]byte, len(b))
|
|
||||||
// copy(blob, b)
|
|
||||||
// // TODO: 2 requests for the same blob could occure at the same time
|
|
||||||
// bcache.Add(a.cluster, blob)
|
|
||||||
// } else {
|
|
||||||
// bi, ok := bcache.Get(a.cluster)
|
|
||||||
// if !ok {
|
|
||||||
// return nil, errors.New("not in cache anymore")
|
|
||||||
// }
|
|
||||||
// blob = bi.([]byte)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// bs, err = readInt32(blob[a.blob*4:a.blob*4+4], nil)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// be, err = readInt32(blob[a.blob*4+4:a.blob*4+4+4], nil)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // avoid retaining all the chunk
|
|
||||||
// c := make([]byte, be-bs)
|
|
||||||
// copy(c, blob[bs:be])
|
|
||||||
// return c, nil
|
|
||||||
|
|
||||||
// } else if compression == 0 || compression == 1 {
|
|
||||||
// // uncompresssed
|
|
||||||
// startPos := start + 1
|
|
||||||
// blobOffset := uint64(a.blob * 4)
|
|
||||||
|
|
||||||
// bs, err := readInt32(a.z.bytesRangeAt(startPos+blobOffset, startPos+blobOffset+4))
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// be, err := readInt32(a.z.bytesRangeAt(startPos+blobOffset+4, startPos+blobOffset+4+4))
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return a.z.bytesRangeAt(startPos+uint64(bs), startPos+uint64(be))
|
|
||||||
// }
|
|
||||||
|
|
||||||
return buff, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ContentEntry) Redirect() (*ContentEntry, error) {
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseContentEntry(offset int64, base *BaseEntry) (*ContentEntry, error) {
|
|
||||||
entry := &ContentEntry{
|
|
||||||
BaseEntry: base,
|
|
||||||
}
|
|
||||||
|
|
||||||
data := make([]byte, 2)
|
|
||||||
if err := r.readRange(offset, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeTypeIndex, err := readUint16(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mimeTypeIndex >= uint16(len(r.mimeTypes)) {
|
|
||||||
return nil, errors.Errorf("mime type index '%d' greater than mime types length '%d'", mimeTypeIndex, len(r.mimeTypes))
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.mimeType = r.mimeTypes[mimeTypeIndex]
|
|
||||||
|
|
||||||
data = make([]byte, 1)
|
|
||||||
if err := r.readRange(offset+3, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.namespace = Namespace(data[0])
|
|
||||||
|
|
||||||
data = make([]byte, 4)
|
|
||||||
if err := r.readRange(offset+8, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterIndex, err := readUint32(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.clusterIndex = clusterIndex
|
|
||||||
|
|
||||||
if err := r.readRange(offset+12, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
blobIndex, err := readUint32(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.blobIndex = blobIndex
|
|
||||||
|
|
||||||
url, read, err := r.readStringAt(offset + 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.url = url
|
|
||||||
|
|
||||||
title, _, err := r.readStringAt(offset + 16 + read)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.title = title
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Namespace string
|
|
||||||
|
|
||||||
const (
|
|
||||||
V6NamespaceContent = "C"
|
|
||||||
V6NamespaceMetadata = "M"
|
|
||||||
V6NamespaceWellKnown = "W"
|
|
||||||
V6NamespaceSearch = "X"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
V5NamespaceLayout = "-"
|
|
||||||
V5NamespaceArticle = "A"
|
|
||||||
V5NamespaceArticleMetadata = "B"
|
|
||||||
V5NamespaceImageFile = "I"
|
|
||||||
V5NamespaceImageText = "J"
|
|
||||||
V5NamespaceMetadata = "M"
|
|
||||||
V5NamespaceCategoryText = "U"
|
|
||||||
V5NamespaceCategoryArticleList = "V"
|
|
||||||
V5NamespaceCategoryPerArticle = "W"
|
|
||||||
V5NamespaceSearch = "X"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Entry interface {
|
|
||||||
Redirect() (*ContentEntry, error)
|
|
||||||
Namespace() Namespace
|
|
||||||
URL() string
|
|
||||||
Title() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseEntry struct {
|
|
||||||
mimeTypeIndex uint16
|
|
||||||
namespace Namespace
|
|
||||||
url string
|
|
||||||
title string
|
|
||||||
reader *Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BaseEntry) Namespace() Namespace {
|
|
||||||
return e.namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BaseEntry) Title() string {
|
|
||||||
if e.title == "" {
|
|
||||||
return e.url
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.title
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BaseEntry) URL() string {
|
|
||||||
return e.url
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseBaseEntry(offset int64) (*BaseEntry, error) {
|
|
||||||
entry := &BaseEntry{
|
|
||||||
reader: r,
|
|
||||||
}
|
|
||||||
|
|
||||||
data := make([]byte, 2)
|
|
||||||
if err := r.readRange(offset, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeTypeIndex, err := readUint16(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.mimeTypeIndex = mimeTypeIndex
|
|
||||||
|
|
||||||
data = make([]byte, 1)
|
|
||||||
if err := r.readRange(offset+3, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.namespace = Namespace(data[0])
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedirectEntry struct {
|
|
||||||
*BaseEntry
|
|
||||||
redirectIndex uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *RedirectEntry) Redirect() (*ContentEntry, error) {
|
|
||||||
if e.redirectIndex >= uint32(len(e.reader.urlIndex)) {
|
|
||||||
return nil, errors.Wrapf(ErrInvalidEntryIndex, "entry index '%d' out of bounds", e.redirectIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryPtr := e.reader.urlIndex[e.redirectIndex]
|
|
||||||
entry, err := e.reader.parseEntryAt(int64(entryPtr))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err = entry.Redirect()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentEntry, ok := entry.(*ContentEntry)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.WithStack(ErrInvalidRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentEntry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseRedirectEntry(offset int64, base *BaseEntry) (*RedirectEntry, error) {
|
|
||||||
entry := &RedirectEntry{
|
|
||||||
BaseEntry: base,
|
|
||||||
}
|
|
||||||
|
|
||||||
data := make([]byte, 4)
|
|
||||||
if err := r.readRange(offset+8, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectIndex, err := readUint32(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.redirectIndex = redirectIndex
|
|
||||||
|
|
||||||
url, read, err := r.readStringAt(offset + 12)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.url = url
|
|
||||||
|
|
||||||
title, _, err := r.readStringAt(offset + 12 + read)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.title = title
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import "github.com/pkg/errors"
|
|
||||||
|
|
||||||
type EntryIterator struct {
|
|
||||||
index int
|
|
||||||
entry Entry
|
|
||||||
err error
|
|
||||||
reader *Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *EntryIterator) Next() bool {
|
|
||||||
if it.err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
entryCount := it.reader.EntryCount()
|
|
||||||
|
|
||||||
if it.index >= int(entryCount-1) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
entry, err := it.reader.EntryAt(it.index)
|
|
||||||
if err != nil {
|
|
||||||
it.err = errors.WithStack(err)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
it.entry = entry
|
|
||||||
it.index++
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *EntryIterator) Err() error {
|
|
||||||
return it.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *EntryIterator) Index() int {
|
|
||||||
return it.index
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *EntryIterator) Entry() Entry {
|
|
||||||
return it.entry
|
|
||||||
}
|
|
@ -2,9 +2,4 @@ package zim
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var ErrNotFound = errors.New("not found")
|
||||||
ErrInvalidEntryIndex = errors.New("invalid entry index")
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
ErrInvalidRedirect = errors.New("invalid redirect")
|
|
||||||
ErrCompressionAlgorithmNotSupported = errors.New("compression algorithm not supported")
|
|
||||||
)
|
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
URLCacheSize int
|
|
||||||
URLCacheTTL time.Duration
|
|
||||||
TitleCacheSize int
|
|
||||||
TitleCacheTTL time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type OptionFunc func(opts *Options)
|
|
||||||
|
|
||||||
func NewOptions(funcs ...OptionFunc) *Options {
|
|
||||||
funcs = append([]OptionFunc{
|
|
||||||
WithURLCacheSize(64),
|
|
||||||
WithTitleCacheSize(64),
|
|
||||||
}, funcs...)
|
|
||||||
|
|
||||||
opts := &Options{}
|
|
||||||
for _, fn := range funcs {
|
|
||||||
fn(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithURLCacheSize(size int) OptionFunc {
|
|
||||||
return func(opts *Options) {
|
|
||||||
opts.URLCacheSize = size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithTitleCacheSize(size int) OptionFunc {
|
|
||||||
return func(opts *Options) {
|
|
||||||
opts.TitleCacheSize = size
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,522 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const zimFormatMagicNumber uint32 = 0x44D495A
|
|
||||||
const nullByte = '\x00'
|
|
||||||
const zimRedirect = 0xffff
|
|
||||||
|
|
||||||
type Reader struct {
|
|
||||||
majorVersion uint16
|
|
||||||
minorVersion uint16
|
|
||||||
uuid string
|
|
||||||
entryCount uint32
|
|
||||||
clusterCount uint32
|
|
||||||
urlPtrPos uint64
|
|
||||||
titlePtrPos uint64
|
|
||||||
clusterPtrPos uint64
|
|
||||||
mimeListPos uint64
|
|
||||||
mainPage uint32
|
|
||||||
layoutPage uint32
|
|
||||||
checksumPos uint64
|
|
||||||
|
|
||||||
mimeTypes []string
|
|
||||||
|
|
||||||
urlIndex []uint64
|
|
||||||
|
|
||||||
urlCache *lru.Cache[string, uint64]
|
|
||||||
titleCache *lru.Cache[string, uint64]
|
|
||||||
|
|
||||||
seeker io.ReadSeekCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) Version() (majorVersion, minorVersion uint16) {
|
|
||||||
return r.majorVersion, r.minorVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) EntryCount() uint32 {
|
|
||||||
return r.entryCount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ClusterCount() uint32 {
|
|
||||||
return r.clusterCount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) UUID() string {
|
|
||||||
return r.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) Entries() *EntryIterator {
|
|
||||||
return &EntryIterator{
|
|
||||||
reader: r,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) EntryAt(idx int) (Entry, error) {
|
|
||||||
if idx >= len(r.urlIndex) || idx < 0 {
|
|
||||||
return nil, errors.Wrapf(ErrInvalidEntryIndex, "index '%d' out of bounds", idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryPtr := r.urlIndex[idx]
|
|
||||||
|
|
||||||
entry, err := r.parseEntryAt(int64(entryPtr))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.cacheEntry(entryPtr, entry)
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) EntryWithURL(ns Namespace, url string) (Entry, error) {
|
|
||||||
offset, found := r.getEntryOffsetByURLFromCache(ns, url)
|
|
||||||
if found {
|
|
||||||
entry, err := r.parseEntryAt(int64(offset))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
iterator := r.Entries()
|
|
||||||
|
|
||||||
for iterator.Next() {
|
|
||||||
entry := iterator.Entry()
|
|
||||||
|
|
||||||
if entry.Namespace() == ns && entry.URL() == url {
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := iterator.Err(); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.WithStack(ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) EntryWithTitle(title string) (Entry, error) {
|
|
||||||
offset, found := r.getEntryOffsetByTitleFromCache(title)
|
|
||||||
if found {
|
|
||||||
entry, err := r.parseEntryAt(int64(offset))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
iterator := r.Entries()
|
|
||||||
|
|
||||||
for iterator.Next() {
|
|
||||||
entry := iterator.Entry()
|
|
||||||
|
|
||||||
if entry.Title() == title {
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := iterator.Err(); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.WithStack(ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) getURLCacheKey(entry Entry) string {
|
|
||||||
return fmt.Sprintf("%s/%s", entry.Namespace(), entry.URL())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) cacheEntry(offset uint64, entry Entry) {
|
|
||||||
urlKey := r.getURLCacheKey(entry)
|
|
||||||
r.urlCache.Add(urlKey, offset)
|
|
||||||
r.titleCache.Add(entry.Title(), offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) getEntryOffsetByURLFromCache(namespace Namespace, url string) (uint64, bool) {
|
|
||||||
key := fmt.Sprintf("%s/%s", namespace, url)
|
|
||||||
return r.urlCache.Get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) getEntryOffsetByTitleFromCache(title string) (uint64, bool) {
|
|
||||||
return r.titleCache.Get(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parse() error {
|
|
||||||
if err := r.parseHeader(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.parseMimeTypes(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.parseURLIndex(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseHeader() error {
|
|
||||||
magicNumber, err := r.readUint32At(0)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if magicNumber != zimFormatMagicNumber {
|
|
||||||
return errors.Errorf("invalid zim magic number '%d'", magicNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
majorVersion, err := r.readUint16At(4)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.majorVersion = majorVersion
|
|
||||||
|
|
||||||
minorVersion, err := r.readUint16At(6)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.minorVersion = minorVersion
|
|
||||||
|
|
||||||
if err := r.parseUUID(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryCount, err := r.readUint32At(24)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.entryCount = entryCount
|
|
||||||
|
|
||||||
clusterCount, err := r.readUint32At(28)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.clusterCount = clusterCount
|
|
||||||
|
|
||||||
urlPtrPos, err := r.readUint64At(32)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.urlPtrPos = urlPtrPos
|
|
||||||
|
|
||||||
titlePtrPos, err := r.readUint64At(40)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.titlePtrPos = titlePtrPos
|
|
||||||
|
|
||||||
clusterPtrPos, err := r.readUint64At(48)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.clusterPtrPos = clusterPtrPos
|
|
||||||
|
|
||||||
mimeListPos, err := r.readUint64At(56)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.mimeListPos = mimeListPos
|
|
||||||
|
|
||||||
mainPage, err := r.readUint32At(64)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.mainPage = mainPage
|
|
||||||
|
|
||||||
layoutPage, err := r.readUint32At(68)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.layoutPage = layoutPage
|
|
||||||
|
|
||||||
checksumPos, err := r.readUint64At(72)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.checksumPos = checksumPos
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseUUID() error {
|
|
||||||
data := make([]byte, 16)
|
|
||||||
if err := r.readRange(8, data); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := make([]string, 0, 5)
|
|
||||||
|
|
||||||
val32, err := readUint32(data[0:4], binary.BigEndian)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = append(parts, fmt.Sprintf("%08x", val32))
|
|
||||||
|
|
||||||
val16, err := readUint16(data[4:6], binary.BigEndian)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = append(parts, fmt.Sprintf("%04x", val16))
|
|
||||||
|
|
||||||
val16, err = readUint16(data[6:8], binary.BigEndian)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = append(parts, fmt.Sprintf("%04x", val16))
|
|
||||||
|
|
||||||
val16, err = readUint16(data[8:10], binary.BigEndian)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = append(parts, fmt.Sprintf("%04x", val16))
|
|
||||||
|
|
||||||
val32, err = readUint32(data[10:14], binary.BigEndian)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
val16, err = readUint16(data[14:16], binary.BigEndian)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = append(parts, fmt.Sprintf("%x%x", val32, val16))
|
|
||||||
|
|
||||||
r.uuid = strings.Join(parts, "-")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseMimeTypes() error {
|
|
||||||
mimeTypes := make([]string, 0)
|
|
||||||
|
|
||||||
offset := int64(r.mimeListPos)
|
|
||||||
for {
|
|
||||||
mimeType, read, err := r.readStringAt(offset)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mimeType == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
mimeTypes = append(mimeTypes, mimeType)
|
|
||||||
|
|
||||||
offset += read + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
r.mimeTypes = mimeTypes
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseURLIndex() error {
|
|
||||||
urlIndex, err := r.parseEntryIndex(int64(r.urlPtrPos))
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.urlIndex = urlIndex
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseEntryAt(offset int64) (Entry, error) {
|
|
||||||
base, err := r.parseBaseEntry(offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var entry Entry
|
|
||||||
|
|
||||||
if base.mimeTypeIndex == zimRedirect {
|
|
||||||
entry, err = r.parseRedirectEntry(offset, base)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entry, err = r.parseContentEntry(offset, base)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) parseEntryIndex(startAddr int64) ([]uint64, error) {
|
|
||||||
index := make([]uint64, r.entryCount)
|
|
||||||
|
|
||||||
data := make([]byte, 8)
|
|
||||||
for i := int64(0); i < int64(r.entryCount); i++ {
|
|
||||||
if err := r.readRange(startAddr+i*8, data); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ptr, err := readUint64(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
index[i] = ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
return index, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readRange(offset int64, v []byte) error {
|
|
||||||
if _, err := r.seeker.Seek(offset, io.SeekStart); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
read, err := r.seeker.Read(v)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if read != len(v) {
|
|
||||||
return errors.New("could not read enough bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readUint32At(offset int64) (uint32, error) {
|
|
||||||
data := make([]byte, 4)
|
|
||||||
if err := r.readRange(offset, data); err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := readUint32(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readUint16At(offset int64) (uint16, error) {
|
|
||||||
data := make([]byte, 2)
|
|
||||||
if err := r.readRange(offset, data); err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := readUint16(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readUint64At(offset int64) (uint64, error) {
|
|
||||||
data := make([]byte, 8)
|
|
||||||
if err := r.readRange(offset, data); err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := readUint64(data, binary.LittleEndian)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readStringAt(offset int64) (string, int64, error) {
|
|
||||||
data := make([]byte, 1)
|
|
||||||
var sb strings.Builder
|
|
||||||
read := int64(0)
|
|
||||||
for {
|
|
||||||
if err := r.readRange(offset+read, data); err != nil {
|
|
||||||
return "", read, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sb.WriteByte(data[0]); err != nil {
|
|
||||||
return "", read, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if data[0] == nullByte {
|
|
||||||
str := strings.TrimRight(sb.String(), "\x00")
|
|
||||||
return str, read, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
read++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) Close() error {
|
|
||||||
if err := r.seeker.Close(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReader(seeker io.ReadSeekCloser, funcs ...OptionFunc) (*Reader, error) {
|
|
||||||
opts := NewOptions(funcs...)
|
|
||||||
|
|
||||||
urlCache, err := lru.New[string, uint64](opts.URLCacheSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
titleCache, err := lru.New[string, uint64](opts.TitleCacheSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := &Reader{
|
|
||||||
seeker: seeker,
|
|
||||||
urlCache: urlCache,
|
|
||||||
titleCache: titleCache,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := reader.parse(); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Open(path string, funcs ...OptionFunc) (*Reader, error) {
|
|
||||||
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := NewReader(file, funcs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader, nil
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReader(t *testing.T) {
|
|
||||||
files, err := filepath.Glob("testdata/*.zim")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, zf := range files {
|
|
||||||
testName := filepath.Base(zf)
|
|
||||||
t.Run(testName, func(t *testing.T) {
|
|
||||||
reader, err := Open(zf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := reader.Close(); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
iterator := reader.Entries()
|
|
||||||
for iterator.Next() {
|
|
||||||
entry := iterator.Entry()
|
|
||||||
|
|
||||||
content, err := entry.Redirect()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%+v", errors.WithStack(err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("%s/%s: %s", content.Namespace(), content.URL(), content.Title())
|
|
||||||
|
|
||||||
contentReader, err := content.Reader()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%+v", errors.WithStack(err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
spew.Dump(contentReader)
|
|
||||||
}
|
|
||||||
if err := iterator.Err(); err != nil {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// entry, err := reader.EntryWithURL(V6NamespaceContent, "A/a.tile.openstreetmap.org/16/33682/22970.png")
|
|
||||||
// if err != nil {
|
|
||||||
// t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// content, err := entry.Redirect()
|
|
||||||
// if err != nil {
|
|
||||||
// t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// contentReader, err := content.Reader()
|
|
||||||
// if err != nil {
|
|
||||||
// t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
|
|
||||||
// data, err := io.ReadAll(contentReader)
|
|
||||||
// if err != nil {
|
|
||||||
// t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
|
|
||||||
// spew.Dump(data)
|
|
||||||
}
|
|
||||||
}
|
|
BIN
pkg/bundle/zim/testdata/cadoles.zim
vendored
BIN
pkg/bundle/zim/testdata/cadoles.zim
vendored
Binary file not shown.
@ -1,52 +0,0 @@
|
|||||||
package zim
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// read a little endian uint64
|
|
||||||
func readUint64(b []byte, order binary.ByteOrder) (uint64, error) {
|
|
||||||
var v uint64
|
|
||||||
buf := bytes.NewBuffer(b)
|
|
||||||
if err := binary.Read(buf, order, &v); err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// read a little endian uint32
|
|
||||||
func readUint32(b []byte, order binary.ByteOrder) (uint32, error) {
|
|
||||||
var v uint32
|
|
||||||
buf := bytes.NewBuffer(b)
|
|
||||||
if err := binary.Read(buf, order, &v); err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// read a little endian uint16
|
|
||||||
func readUint16(b []byte, order binary.ByteOrder) (uint16, error) {
|
|
||||||
var v uint16
|
|
||||||
buf := bytes.NewBuffer(b)
|
|
||||||
if err := binary.Read(buf, order, &v); err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// read a little endian uint8
|
|
||||||
func readUint8(b []byte, order binary.ByteOrder) (uint8, error) {
|
|
||||||
var v uint8
|
|
||||||
buf := bytes.NewBuffer(b)
|
|
||||||
if err := binary.Read(buf, order, &v); err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -101,7 +100,6 @@ func (z *ZimReader) ListArticles() <-chan *Article {
|
|||||||
|
|
||||||
if art == nil {
|
if art == nil {
|
||||||
// TODO: deal with redirect continue
|
// TODO: deal with redirect continue
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
ch <- art
|
ch <- art
|
||||||
}
|
}
|
||||||
@ -298,8 +296,6 @@ func (z *ZimReader) readFileHeaders() error {
|
|||||||
}
|
}
|
||||||
z.layoutPage = v
|
z.layoutPage = v
|
||||||
|
|
||||||
spew.Dump(z)
|
|
||||||
|
|
||||||
z.MimeTypes()
|
z.MimeTypes()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
150
pkg/bundle/zim/zim_test.go
Normal file
150
pkg/bundle/zim/zim_test.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Z *ZimReader
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
Z, err = NewReader("testdata/wikibooks_af_all_maxi_2023-06.zim")
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("Can't read %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpen(t *testing.T) {
|
||||||
|
if Z.ArticleCount == 0 {
|
||||||
|
t.Errorf("No article found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMime(t *testing.T) {
|
||||||
|
if len(Z.MimeTypes()) == 0 {
|
||||||
|
t.Errorf("No mime types found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayInfost(t *testing.T) {
|
||||||
|
info := Z.String()
|
||||||
|
if len(info) < 0 {
|
||||||
|
t.Errorf("Can't read infos")
|
||||||
|
}
|
||||||
|
t.Log(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLAtIdx(t *testing.T) {
|
||||||
|
// addr 0 is a redirect
|
||||||
|
p, _ := Z.OffsetAtURLIdx(5)
|
||||||
|
a, _ := Z.ArticleAt(p)
|
||||||
|
if a == nil {
|
||||||
|
t.Errorf("Can't find 1st url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayArticle(t *testing.T) {
|
||||||
|
// addr 0 is a redirect
|
||||||
|
p, _ := Z.OffsetAtURLIdx(5)
|
||||||
|
a, _ := Z.ArticleAt(p)
|
||||||
|
if a == nil {
|
||||||
|
t.Errorf("Can't find 1st url")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPageNoIndex(t *testing.T) {
|
||||||
|
a, _ := Z.GetPageNoIndex("A/Dracula:Capitol_1.html")
|
||||||
|
if a == nil {
|
||||||
|
t.Errorf("Can't find existing url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListArticles(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping test in short mode.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var i uint32
|
||||||
|
|
||||||
|
for a := range Z.ListArticles() {
|
||||||
|
i++
|
||||||
|
t.Log(a.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
t.Errorf("Can't find any urls")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != Z.ArticleCount-1 {
|
||||||
|
t.Errorf("Can't find the exact ArticleCount urls %d vs %d", i, Z.ArticleCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMainPage(t *testing.T) {
|
||||||
|
a, _ := Z.MainPage()
|
||||||
|
if a == nil {
|
||||||
|
t.Errorf("Can't find the mainpage article")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFavicon(t *testing.T) {
|
||||||
|
favicon, err := Z.Favicon()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
if favicon == nil {
|
||||||
|
t.Errorf("Can't find the favicon article")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadata(t *testing.T) {
|
||||||
|
metadata, err := Z.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
if metadata == nil {
|
||||||
|
t.Errorf("Can't find the metadata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestData(t *testing.T) {
|
||||||
|
// addr 0 is a redirect
|
||||||
|
p, _ := Z.OffsetAtURLIdx(2)
|
||||||
|
a, _ := Z.ArticleAt(p)
|
||||||
|
b, _ := a.Data()
|
||||||
|
data := string(b)
|
||||||
|
if a.EntryType != RedirectEntry {
|
||||||
|
if len(data) == 0 {
|
||||||
|
t.Error("can't read data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Log(a.String())
|
||||||
|
t.Log(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkArticleBytes(b *testing.B) {
|
||||||
|
// addr 0 is a redirect
|
||||||
|
p, _ := Z.OffsetAtURLIdx(5)
|
||||||
|
a, _ := Z.ArticleAt(p)
|
||||||
|
if a == nil {
|
||||||
|
b.Errorf("Can't find 1st url")
|
||||||
|
}
|
||||||
|
data, err := a.Data()
|
||||||
|
if err != nil {
|
||||||
|
b.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.SetBytes(int64(len(data)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
a.Data()
|
||||||
|
bcache.Purge() // prevent memiozing value
|
||||||
|
}
|
||||||
|
}
|
@ -97,10 +97,6 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
|||||||
bus: opts.Bus,
|
bus: opts.Bus,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, middleware := range opts.HTTPMiddlewares {
|
|
||||||
router.Use(middleware)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Route("/edge", func(r chi.Router) {
|
router.Route("/edge", func(r chi.Router) {
|
||||||
r.Route("/sdk", func(r chi.Router) {
|
r.Route("/sdk", func(r chi.Router) {
|
||||||
r.Get("/client.js", handler.handleSDKClient)
|
r.Get("/client.js", handler.handleSDKClient)
|
||||||
|
@ -18,7 +18,6 @@ type HandlerOptions struct {
|
|||||||
UploadMaxFileSize int64
|
UploadMaxFileSize int64
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
HTTPMounts []func(r chi.Router)
|
HTTPMounts []func(r chi.Router)
|
||||||
HTTPMiddlewares []func(next http.Handler) http.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultHandlerOptions() *HandlerOptions {
|
func defaultHandlerOptions() *HandlerOptions {
|
||||||
@ -36,7 +35,6 @@ func defaultHandlerOptions() *HandlerOptions {
|
|||||||
Timeout: time.Second * 30,
|
Timeout: time.Second * 30,
|
||||||
},
|
},
|
||||||
HTTPMounts: make([]func(r chi.Router), 0),
|
HTTPMounts: make([]func(r chi.Router), 0),
|
||||||
HTTPMiddlewares: make([]func(http.Handler) http.Handler, 0),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,9 +75,3 @@ func WithHTTPMounts(mounts ...func(r chi.Router)) HandlerOptionFunc {
|
|||||||
opts.HTTPMounts = mounts
|
opts.HTTPMounts = mounts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) HandlerOptionFunc {
|
|
||||||
return func(opts *HandlerOptions) {
|
|
||||||
opts.HTTPMiddlewares = middlewares
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
package jwtutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddKeyWithSigningAlgo(keySet jwk.Set, key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm) error {
|
|
||||||
addedKey := key
|
|
||||||
|
|
||||||
if !strings.HasPrefix(string(signingAlgorithm), "HS") {
|
|
||||||
publicKey, err := key.PublicKey()
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
addedKey = publicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := addedKey.Set(jwk.AlgorithmKey, signingAlgorithm); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := keySet.AddKey(addedKey); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,119 +0,0 @@
|
|||||||
package jwtutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
|
||||||
"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 := Parse([]byte(rawToken), keySet)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package jwtutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jws"
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SignedToken(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, claims map[string]any) ([]byte, error) {
|
|
||||||
token := jwt.New()
|
|
||||||
|
|
||||||
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
|
|
||||||
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 {
|
|
||||||
if err := token.Set(key, value); err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "could not set claim '%s' with value '%v'", key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := token.Set(jwk.AlgorithmKey, signingAlgorithm); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rawToken, err := jwt.Sign(token, jwt.WithKey(signingAlgorithm, key))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Parse(rawToken []byte, keySet jwk.Set) (jwt.Token, error) {
|
|
||||||
token, err := jwt.Parse(rawToken,
|
|
||||||
jwt.WithKeySet(keySet, jws.WithRequireKid(false)),
|
|
||||||
jwt.WithValidate(true),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, nil
|
|
||||||
}
|
|
@ -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")
|
|
35
pkg/module/auth/http/jwt.go
Normal file
35
pkg/module/auth/http/jwt.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) {
|
||||||
|
token := jwt.New()
|
||||||
|
|
||||||
|
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range claims {
|
||||||
|
if err := token.Set(key, value); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not set claim '%s' with value '%v'", key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := token.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawToken, err := jwt.Sign(token, jwt.WithKey(algo, key))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawToken, nil
|
||||||
|
}
|
@ -7,7 +7,6 @@ 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"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@ -32,8 +31,8 @@ func init() {
|
|||||||
|
|
||||||
type LocalHandler struct {
|
type LocalHandler struct {
|
||||||
router chi.Router
|
router chi.Router
|
||||||
|
algo jwa.KeyAlgorithm
|
||||||
key jwk.Key
|
key jwk.Key
|
||||||
signingAlgorithm jwa.SignatureAlgorithm
|
|
||||||
getCookieDomain GetCookieDomainFunc
|
getCookieDomain GetCookieDomainFunc
|
||||||
cookieDuration time.Duration
|
cookieDuration time.Duration
|
||||||
accounts map[string]LocalAccount
|
accounts map[string]LocalAccount
|
||||||
@ -113,7 +112,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 := 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,15 +181,15 @@ 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{
|
||||||
|
algo: algo,
|
||||||
key: key,
|
key: key,
|
||||||
signingAlgorithm: signingAlgorithm,
|
|
||||||
accounts: toAccountsMap(opts.Accounts),
|
accounts: toAccountsMap(opts.Accounts),
|
||||||
getCookieDomain: opts.GetCookieDomain,
|
getCookieDomain: opts.GetCookieDomain,
|
||||||
cookieDuration: opts.CookieDuration,
|
cookieDuration: opts.CookieDuration,
|
||||||
|
109
pkg/module/auth/jwt.go
Normal file
109
pkg/module/auth/jwt.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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 FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, 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 nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cookie != nil {
|
||||||
|
rawToken = cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawToken == "" {
|
||||||
|
return nil, errors.WithStack(ErrUnauthenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -1,117 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const AnonIssuer = "anon"
|
|
||||||
|
|
||||||
func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler {
|
|
||||||
opts := defaultAnonymousUserOptions()
|
|
||||||
for _, fn := range funcs {
|
|
||||||
fn(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
rawToken, err := jwtutil.FindRawToken(r, jwtutil.WithFinders(
|
|
||||||
jwtutil.FindTokenFromAuthorizationHeader,
|
|
||||||
jwtutil.FindTokenFromQueryString(auth.CookieName),
|
|
||||||
jwtutil.FindTokenFromCookie(auth.CookieName),
|
|
||||||
))
|
|
||||||
|
|
||||||
// If request already has a raw token, we do nothing
|
|
||||||
if rawToken != "" && err == nil {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
uuid, err := uuid.NewUUID()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not generate uuid for anonymous user", logger.E(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
|
|
||||||
preferredUsername, err := generateRandomPreferredUsername(8)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not generate preferred username for anonymous user", logger.E(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claims := map[string]any{
|
|
||||||
auth.ClaimSubject: subject,
|
|
||||||
auth.ClaimIssuer: AnonIssuer,
|
|
||||||
auth.ClaimPreferredUsername: preferredUsername,
|
|
||||||
auth.ClaimEdgeRole: opts.Role,
|
|
||||||
auth.ClaimEdgeEntrypoint: opts.Entrypoint,
|
|
||||||
auth.ClaimEdgeTenant: opts.Tenant,
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := jwtutil.SignedToken(key, signingAlgorithm, claims)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieDomain, err := opts.GetCookieDomain(r)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not retrieve cookie domain", logger.E(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie := http.Cookie{
|
|
||||||
Name: auth.CookieName,
|
|
||||||
Value: string(token),
|
|
||||||
Domain: cookieDomain,
|
|
||||||
HttpOnly: false,
|
|
||||||
Expires: time.Now().Add(opts.CookieDuration),
|
|
||||||
Path: "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, &cookie)
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.HandlerFunc(handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateRandomPreferredUsername(size int) (string, error) {
|
|
||||||
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
||||||
max := big.NewInt(int64(len(letters)))
|
|
||||||
|
|
||||||
b := make([]rune, size)
|
|
||||||
for i := range b {
|
|
||||||
idx, err := rand.Int(rand.Reader, max)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithStack(err)
|
|
||||||
}
|
|
||||||
b[i] = letters[idx.Int64()]
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("Anon %s", string(b)), nil
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetCookieDomainFunc func(r *http.Request) (string, error)
|
|
||||||
|
|
||||||
func defaultGetCookieDomain(r *http.Request) (string, error) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnonymousUserOptions struct {
|
|
||||||
GetCookieDomain GetCookieDomainFunc
|
|
||||||
CookieDuration time.Duration
|
|
||||||
Tenant string
|
|
||||||
Entrypoint string
|
|
||||||
Role string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnonymousUserOptionFunc func(*AnonymousUserOptions)
|
|
||||||
|
|
||||||
func defaultAnonymousUserOptions() *AnonymousUserOptions {
|
|
||||||
return &AnonymousUserOptions{
|
|
||||||
GetCookieDomain: defaultGetCookieDomain,
|
|
||||||
CookieDuration: 24 * time.Hour,
|
|
||||||
Tenant: "",
|
|
||||||
Entrypoint: "",
|
|
||||||
Role: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc {
|
|
||||||
return func(opts *AnonymousUserOptions) {
|
|
||||||
opts.GetCookieDomain = getCookieDomain
|
|
||||||
opts.CookieDuration = duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithTenant(tenant string) AnonymousUserOptionFunc {
|
|
||||||
return func(opts *AnonymousUserOptions) {
|
|
||||||
opts.Tenant = tenant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc {
|
|
||||||
return func(opts *AnonymousUserOptions) {
|
|
||||||
opts.Entrypoint = entrypoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithRole(role string) AnonymousUserOptionFunc {
|
|
||||||
return func(opts *AnonymousUserOptions) {
|
|
||||||
opts.Role = role
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
||||||
@ -27,7 +22,7 @@ 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 {
|
||||||
@ -95,7 +94,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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,19 +12,16 @@ 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...)
|
||||||
|
|
||||||
for _, name := range h.profileClaims {
|
|
||||||
value, err := h.getClaim(ctx, r, name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, jwtutil.ErrUnauthenticated) {
|
if errors.Is(err, ErrUnauthenticated) {
|
||||||
api.ErrorResponse(
|
api.ErrorResponse(
|
||||||
w, http.StatusUnauthorized,
|
w, http.StatusUnauthorized,
|
||||||
api.ErrCodeUnauthorized,
|
api.ErrCodeUnauthorized,
|
||||||
@ -45,7 +41,10 @@ func (h *Handler) serveProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
profile[name] = value
|
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) {
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
@ -280,13 +279,13 @@ 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,
|
||||||
|
@ -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 {
|
@ -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)
|
13
pkg/module/share/sqlite/module_test.go
Normal file
13
pkg/module/share/sqlite/module_test.go
Normal 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)
|
||||||
|
}
|
@ -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{}
|
@ -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
|
||||||
}
|
}
|
1
pkg/module/share/sqlite/testdata/.gitignore
vendored
Normal file
1
pkg/module/share/sqlite/testdata/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
@ -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))
|
||||||
}
|
}
|
16
pkg/module/share/testsuite/repository.go
Normal file
16
pkg/module/share/testsuite/repository.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
2
pkg/sdk/client/dist/client.js
vendored
2
pkg/sdk/client/dist/client.js
vendored
@ -92,7 +92,7 @@ var Edge=(()=>{var K3=Object.create;var Mi=Object.defineProperty,Y3=Object.defin
|
|||||||
</edge-menu-item>
|
</edge-menu-item>
|
||||||
`}_canAccess(t){var a,o;let i=((a=this._profile)==null?void 0:a.edge_role)||"visitor",n=((o=t.metadata)==null?void 0:o.minimumRole)||"visitor";return sb[i]>=sb[n]}_renderProfile(){let t=this._profile;return re`
|
`}_canAccess(t){var a,o;let i=((a=this._profile)==null?void 0:a.edge_role)||"visitor",n=((o=t.metadata)==null?void 0:o.minimumRole)||"visitor";return sb[i]>=sb[n]}_renderProfile(){let t=this._profile;return re`
|
||||||
<edge-menu-item name='profile' label="${(t==null?void 0:t.preferred_username)||"Profile"}" icon-url='${Zm}'>
|
<edge-menu-item name='profile' label="${(t==null?void 0:t.preferred_username)||"Profile"}" icon-url='${Zm}'>
|
||||||
${t&&t.iss!="anon"?re`<edge-menu-sub-item name='login' label='Logout' icon-url='${nb}' link-url='/edge/auth/logout'></edge-menu-sub-item>`:re`<edge-menu-sub-item name='login' label='Login' icon-url='${tb}' link-url='/edge/auth/login'></edge-menu-sub-item>`}
|
${t?re`<edge-menu-sub-item name='login' label='Logout' icon-url='${nb}' link-url='/edge/auth/logout'></edge-menu-sub-item>`:re`<edge-menu-sub-item name='login' label='Login' icon-url='${tb}' link-url='/edge/auth/login'></edge-menu-sub-item>`}
|
||||||
</edge-menu-item>
|
</edge-menu-item>
|
||||||
`}_handleMenuItemSelected(t){let i=t.detail.element;i.classList.add("selected"),i.classList.remove("unselected");for(let n,a=0;n=this._menuItems[a];a++)n!==i&&(n.unselect(),n.classList.add("unselected"))}_handleMenuItemUnselected(t){if(t.detail.element.classList.remove("selected"),this.renderRoot.querySelectorAll("edge-menu-item.selected").length===0)for(let a,o=0;a=this._menuItems[o];o++)a.classList.remove("unselected")}};le.styles=Ti`
|
`}_handleMenuItemSelected(t){let i=t.detail.element;i.classList.add("selected"),i.classList.remove("unselected");for(let n,a=0;n=this._menuItems[a];a++)n!==i&&(n.unselect(),n.classList.add("unselected"))}_handleMenuItemUnselected(t){if(t.detail.element.classList.remove("selected"),this.renderRoot.querySelectorAll("edge-menu-item.selected").length===0)for(let a,o=0;a=this._menuItems[o];o++)a.classList.remove("unselected")}};le.styles=Ti`
|
||||||
:host {
|
:host {
|
||||||
|
4
pkg/sdk/client/dist/client.js.map
vendored
4
pkg/sdk/client/dist/client.js.map
vendored
File diff suppressed because one or more lines are too long
@ -191,7 +191,7 @@ export class Menu extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
|
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
|
||||||
${
|
${
|
||||||
profile && profile.iss != "anon" ?
|
profile ?
|
||||||
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
|
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
|
||||||
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
|
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package driver
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var ErrSchemeNotRegistered = errors.New("scheme was not registered")
|
|
@ -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{}
|
|
||||||
)
|
|
@ -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
|
|
||||||
}
|
|
@ -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{}
|
|
@ -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
|
|
||||||
}
|
|
@ -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{}
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user