Compare commits
19 Commits
f3c5eee8c8
...
7a703f30cc
Author | SHA1 | Date | |
---|---|---|---|
7a703f30cc | |||
8facff2bd2 | |||
8a5a1cd482 | |||
3fd25988cf | |||
ebe3e77879 | |||
3078ea7d21 | |||
4c6e979bb6 | |||
0fded0170a | |||
6ddd831025 | |||
4fe68e335a | |||
599ff749d3 | |||
9f89c89fb9 | |||
d2472623f2 | |||
c63af872ea | |||
8e574c299b | |||
c3535a4a9b | |||
7e58551f6a | |||
41d5db6321 | |||
8eb441daee |
@ -1 +1,4 @@
|
|||||||
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"
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -4,4 +4,10 @@
|
|||||||
/tools
|
/tools
|
||||||
*.sqlite
|
*.sqlite
|
||||||
/.gitea-release
|
/.gitea-release
|
||||||
/.edge
|
/.edge
|
||||||
|
/data
|
||||||
|
.mktools/
|
||||||
|
/dist
|
||||||
|
/.chglog
|
||||||
|
/CHANGELOG.md
|
||||||
|
/storage-server.key
|
117
.goreleaser.yml
Normal file
117
.goreleaser.yml
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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,7 +34,8 @@ pipeline {
|
|||||||
passwordVariable: 'GITEA_RELEASE_PASSWORD'
|
passwordVariable: 'GITEA_RELEASE_PASSWORD'
|
||||||
])
|
])
|
||||||
]) {
|
]) {
|
||||||
sh 'make gitea-release'
|
sh 'make .mktools'
|
||||||
|
sh "export MKT_PROJECT_VERSION_BRANCH_NAME=${env.BRANCH_NAME}; make gitea-release"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
Makefile
61
Makefile
@ -6,14 +6,17 @@ 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
|
||||||
@ -22,17 +25,23 @@ watch: tools/modd/bin/modd
|
|||||||
test: test-go
|
test: test-go
|
||||||
|
|
||||||
test-go:
|
test-go:
|
||||||
go test -v -count=1 $(GOTEST_ARGS) ./...
|
go test -count=1 $(GOTEST_ARGS) ./...
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run --enable-all $(LINT_ARGS)
|
golangci-lint run --enable-all $(LINT_ARGS)
|
||||||
|
|
||||||
build-edge-cli: build-sdk
|
build-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
|
||||||
|
|
||||||
@ -68,25 +77,31 @@ node_modules:
|
|||||||
run-app: .env
|
run-app: .env
|
||||||
( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS )
|
( set -o allexport && source .env && set +o allexport && bin/cli app run -p $(APP_PATH) $$RUN_APP_ARGS )
|
||||||
|
|
||||||
|
run-storage-server: .env
|
||||||
|
( set -o allexport && source .env && set +o allexport && bin/storage-server run $$RUN_STORAGE_SERVER_ARGS )
|
||||||
|
|
||||||
.env:
|
.env:
|
||||||
cp .env.dist .env
|
cp .env.dist .env
|
||||||
|
|
||||||
gitea-release: tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh build
|
gitea-release: .mktools tools/yq/bin/yq tools/gitea-release/bin/gitea-release.sh goreleaser build
|
||||||
mkdir -p .gitea-release
|
mkdir -p .gitea-release
|
||||||
rm -rf .gitea-release/*
|
rm -rf .gitea-release/*
|
||||||
|
|
||||||
cp bin/cli .gitea-release/edge_cli_amd64
|
cp dist/*.deb .gitea-release/
|
||||||
|
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 = "$(FULL_VERSION)"' ./misc/client-sdk-testsuite/dist/manifest.yml
|
tools/yq/bin/yq -i '.version = "$(MKT_PROJECT_VERSION)"' ./misc/client-sdk-testsuite/dist/manifest.yml
|
||||||
.gitea-release/edge_cli_amd64 app package -d ./misc/client-sdk-testsuite/dist -o .gitea-release
|
bin/cli 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="$(FULL_VERSION)" \
|
GITEA_RELEASE_VERSION="$(MKT_PROJECT_VERSION)" \
|
||||||
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
|
GITEA_RELEASE_NAME="$(MKT_PROJECT_VERSION)" \
|
||||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
GITEA_RELEASE_COMMITISH_TARGET="$$(git rev-parse HEAD)" \
|
||||||
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="" \
|
||||||
@ -105,4 +120,22 @@ 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,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -17,19 +16,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"
|
||||||
@ -44,8 +43,16 @@ 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",
|
||||||
@ -74,14 +81,22 @@ func RunCommand() *cli.Command {
|
|||||||
Value: 0,
|
Value: 0,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "storage-file",
|
Name: "blobstore-dsn",
|
||||||
Usage: "use `FILE` for SQLite storage database",
|
Usage: "use `DSN` for blob storage",
|
||||||
Value: ".edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
|
EnvVars: []string{"EDGE_BLOBSTORE_DSN"},
|
||||||
|
Value: "sqlite://.edge/%APPID%/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "shared-resources-file",
|
Name: "documentstore-dsn",
|
||||||
Usage: "use `FILE` for SQLite shared resources database",
|
Usage: "use `DSN` for document storage",
|
||||||
Value: ".edge/shared-resources.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
|
EnvVars: []string{"EDGE_DOCUMENTSTORE_DSN"},
|
||||||
|
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",
|
||||||
@ -95,9 +110,10 @@ func RunCommand() *cli.Command {
|
|||||||
|
|
||||||
logFormat := ctx.String("log-format")
|
logFormat := ctx.String("log-format")
|
||||||
logLevel := ctx.Int("log-level")
|
logLevel := ctx.Int("log-level")
|
||||||
storageFile := ctx.String("storage-file")
|
blobstoreDSN := ctx.String("blobstore-dsn")
|
||||||
|
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))
|
||||||
@ -143,7 +159,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, storageFile, accountsFile, appsRepository, sharedResourcesFile); err != nil {
|
if err := runApp(appCtx, path, address, documentstoreDSN, blobstoreDSN, shareStoreDSN, accountsFile, appsRepository); 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)
|
||||||
@ -156,7 +172,7 @@ func RunCommand() *cli.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runApp(ctx context.Context, path string, address string, storageFile string, accountsFile string, appRepository appModule.Repository, sharedResourcesFile string) error {
|
func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN, shareStoreDSN, accountsFile string, appRepository appModule.Repository) 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)
|
||||||
@ -181,17 +197,17 @@ func runApp(ctx context.Context, path string, address string, storageFile string
|
|||||||
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 := dummyKey()
|
key, err := jwtutil.NewSymmetricKey(dummySecret)
|
||||||
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(storageFile, manifest.ID),
|
initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN, manifest.ID),
|
||||||
initAccounts(accountsFile, manifest.ID),
|
initAccounts(accountsFile, manifest.ID),
|
||||||
initShareRepository(sharedResourcesFile),
|
|
||||||
initAppRepository(appRepository),
|
initAppRepository(appRepository),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,13 +224,19 @@ func runApp(ctx context.Context, path string, address string, storageFile string
|
|||||||
appModule.Mount(appRepository),
|
appModule.Mount(appRepository),
|
||||||
authModule.Mount(
|
authModule.Mount(
|
||||||
authHTTP.NewLocalHandler(
|
authHTTP.NewLocalHandler(
|
||||||
jwa.HS256, key,
|
key,
|
||||||
|
jwa.HS256,
|
||||||
authHTTP.WithRoutePrefix("/auth"),
|
authHTTP.WithRoutePrefix("/auth"),
|
||||||
authHTTP.WithAccounts(deps.Accounts...),
|
authHTTP.WithAccounts(deps.Accounts...),
|
||||||
),
|
),
|
||||||
authModule.WithJWT(dummyKeySet),
|
authModule.WithJWT(func() (jwk.Set, error) {
|
||||||
|
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")
|
||||||
@ -237,13 +259,13 @@ func runApp(ctx context.Context, path string, address string, storageFile string
|
|||||||
}
|
}
|
||||||
|
|
||||||
type moduleDeps struct {
|
type moduleDeps struct {
|
||||||
AppID app.ID
|
AppID app.ID
|
||||||
Bus bus.Bus
|
Bus bus.Bus
|
||||||
DocumentStore storage.DocumentStore
|
DocumentStore storage.DocumentStore
|
||||||
BlobStore storage.BlobStore
|
BlobStore storage.BlobStore
|
||||||
AppRepository appModule.Repository
|
AppRepository appModule.Repository
|
||||||
ShareRepository shareModule.Repository
|
ShareStore share.Store
|
||||||
Accounts []authHTTP.LocalAccount
|
Accounts []authHTTP.LocalAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModuleDepFunc func(*moduleDeps) error
|
type ModuleDepFunc func(*moduleDeps) error
|
||||||
@ -259,44 +281,16 @@ 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(dummyKeySet),
|
authModule.WithJWT(func() (jwk.Set, error) {
|
||||||
|
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.ShareRepository),
|
shareModule.ModuleFactory(deps.AppID, deps.ShareStore),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@ -317,10 +311,10 @@ func loadLocalAccounts(path string) ([]authHTTP.LocalAccount, error) {
|
|||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
if err := ioutil.WriteFile(path, defaultAccounts, 0o640); err != nil {
|
if err := os.WriteFile(path, defaultAccounts, 0o640); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,6 +412,13 @@ 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
|
||||||
@ -431,21 +432,32 @@ func initMemoryBus(deps *moduleDeps) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDatastores(storageFile string, appID app.ID) ModuleDepFunc {
|
func initDatastores(documentStoreDSN, blobStoreDSN, shareStoreDSN string, appID app.ID) ModuleDepFunc {
|
||||||
return func(deps *moduleDeps) error {
|
return func(deps *moduleDeps) error {
|
||||||
storageFile = injectAppID(storageFile, appID)
|
documentStoreDSN = injectAppID(documentStoreDSN, appID)
|
||||||
|
|
||||||
if err := ensureDir(storageFile); err != nil {
|
documentStore, err := driver.NewDocumentStore(documentStoreDSN)
|
||||||
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 = storageSqlite.NewDocumentStoreWithDB(db)
|
deps.DocumentStore = documentStore
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -465,17 +477,3 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
75
cmd/storage-server/command/auth/check_token.go
Normal file
75
cmd/storage-server/command/auth/check_token.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
58
cmd/storage-server/command/auth/new_token.go
Normal file
58
cmd/storage-server/command/auth/new_token.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
16
cmd/storage-server/command/auth/root.go
Normal file
16
cmd/storage-server/command/auth/root.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
43
cmd/storage-server/command/flag/flag.go
Normal file
43
cmd/storage-server/command/flag/flag.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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)
|
||||||
|
}
|
48
cmd/storage-server/command/main.go
Normal file
48
cmd/storage-server/command/main.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
293
cmd/storage-server/command/run.go
Normal file
293
cmd/storage-server/command/run.go
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
13
cmd/storage-server/main.go
Normal file
13
cmd/storage-server/main.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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,6 +2,14 @@
|
|||||||
|
|
||||||
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,12 +43,6 @@ 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
|
||||||
|
18
go.mod
18
go.mod
@ -1,10 +1,14 @@
|
|||||||
module forge.cadoles.com/arcad/edge
|
module forge.cadoles.com/arcad/edge
|
||||||
|
|
||||||
go 1.19
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
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/lestrrat-go/jwx/v2 v2.0.8
|
github.com/lestrrat-go/jwx/v2 v2.0.8
|
||||||
|
github.com/ulikunitz/xz v0.5.11
|
||||||
modernc.org/sqlite v1.20.4
|
modernc.org/sqlite v1.20.4
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,7 +47,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 // indirect
|
github.com/google/uuid v1.3.0
|
||||||
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
|
||||||
@ -59,12 +63,12 @@ require (
|
|||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
|
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
|
||||||
go.opencensus.io v0.22.5 // indirect
|
go.opencensus.io v0.22.5 // indirect
|
||||||
golang.org/x/crypto v0.7.0
|
golang.org/x/crypto v0.10.0
|
||||||
golang.org/x/mod v0.10.0
|
golang.org/x/mod v0.10.0
|
||||||
golang.org/x/net v0.9.0 // indirect
|
golang.org/x/net v0.11.0
|
||||||
golang.org/x/sys v0.7.0 // indirect
|
golang.org/x/sys v0.9.0 // indirect
|
||||||
golang.org/x/term v0.7.0 // indirect
|
golang.org/x/term v0.9.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.10.0 // indirect
|
||||||
golang.org/x/tools v0.8.0 // indirect
|
golang.org/x/tools v0.8.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
28
go.sum
28
go.sum
@ -188,6 +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.7/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=
|
||||||
@ -201,7 +203,11 @@ 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/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
@ -277,6 +283,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
||||||
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
@ -306,8 +314,8 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -379,8 +387,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -442,13 +450,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -458,8 +466,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
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>
|
||||||
@ -31,6 +32,7 @@
|
|||||||
<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();
|
||||||
|
|
||||||
@ -44,6 +46,7 @@
|
|||||||
.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>
|
29
misc/client-sdk-testsuite/src/public/test/share-module.js
Normal file
29
misc/client-sdk-testsuite/src/public/test/share-module.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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)
|
||||||
|
});
|
||||||
|
});
|
7
misc/client-sdk-testsuite/src/public/test/util.js
Normal file
7
misc/client-sdk-testsuite/src/public/test/util.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
(function(TestUtil) {
|
||||||
|
TestUtil.serverSideCall = (module, func, ...args) => {
|
||||||
|
return Edge.Client.rpc('serverSideCall', { module, func, args })
|
||||||
|
}
|
||||||
|
console.log(TestUtil)
|
||||||
|
|
||||||
|
}(globalThis.TestUtil = globalThis.TestUtil || {}));
|
@ -15,6 +15,8 @@ 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
|
||||||
@ -103,4 +105,9 @@ 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);
|
||||||
}
|
}
|
75
misc/packaging/common/postinstall-storage-server.sh
Normal file
75
misc/packaging/common/postinstall-storage-server.sh
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/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
|
9
misc/packaging/openrc/storage-server.conf
Normal file
9
misc/packaging/openrc/storage-server.conf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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
|
11
misc/packaging/openrc/storage-server.openrc.sh
Normal file
11
misc/packaging/openrc/storage-server.openrc.sh
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#!/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
|
||||||
|
}
|
9
misc/packaging/systemd/storage-server.env
Normal file
9
misc/packaging/systemd/storage-server.env
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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
|
35
misc/packaging/systemd/storage-server.systemd.service
Normal file
35
misc/packaging/systemd/storage-server.systemd.service
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[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,13 +2,18 @@
|
|||||||
**/*.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/src/**/*
|
misc/client-sdk-testsuite/dist/server/*.js
|
||||||
modd.conf
|
modd.conf
|
||||||
|
.env
|
||||||
{
|
{
|
||||||
prep: make build-sdk
|
prep: make build-sdk build-cli build-storage-server
|
||||||
prep: make build-client-sdk-test-app
|
|
||||||
prep: make build
|
|
||||||
daemon: make run-app
|
daemon: make run-app
|
||||||
|
daemon: make run-storage-server
|
||||||
|
}
|
||||||
|
|
||||||
|
misc/client-sdk-testsuite/src/**/*
|
||||||
|
{
|
||||||
|
prep: make build-client-sdk-test-app
|
||||||
}
|
}
|
||||||
|
|
||||||
**/*.go {
|
**/*.go {
|
||||||
|
@ -13,6 +13,7 @@ type ArchiveExt string
|
|||||||
const (
|
const (
|
||||||
ExtZip ArchiveExt = "zip"
|
ExtZip ArchiveExt = "zip"
|
||||||
ExtTarGz ArchiveExt = "tar.gz"
|
ExtTarGz ArchiveExt = "tar.gz"
|
||||||
|
ExtZim ArchiveExt = "zim"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FromPath(path string) (Bundle, error) {
|
func FromPath(path string) (Bundle, error) {
|
||||||
@ -56,5 +57,14 @@ func matchArchivePattern(archivePath string) (Bundle, error) {
|
|||||||
return NewZipBundle(archivePath), nil
|
return NewZipBundle(archivePath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZim), base)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not match file archive '%s'", archivePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
return NewZimBundle(archivePath), nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil, errors.WithStack(ErrUnknownBundleArchiveExt)
|
return nil, errors.WithStack(ErrUnknownBundleArchiveExt)
|
||||||
}
|
}
|
||||||
|
283
pkg/bundle/oldzim/article.go
Normal file
283
pkg/bundle/oldzim/article.go
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RedirectEntry uint16 = 0xffff
|
||||||
|
LinkTargetEntry = 0xfffe
|
||||||
|
DeletedEntry = 0xfffd
|
||||||
|
)
|
||||||
|
|
||||||
|
var articlePool sync.Pool
|
||||||
|
|
||||||
|
// the recent uncompressed blobs, mainly useful while indexing and asking
|
||||||
|
// for the same blob again and again
|
||||||
|
var bcache *lru.Cache[any, any]
|
||||||
|
|
||||||
|
type Article struct {
|
||||||
|
// EntryType is a RedirectEntry/LinkTargetEntry/DeletedEntry or an idx
|
||||||
|
// pointing to ZimReader.mimeTypeList
|
||||||
|
EntryType uint16
|
||||||
|
Title string
|
||||||
|
URLPtr uint64
|
||||||
|
Namespace byte
|
||||||
|
url string
|
||||||
|
blob uint32
|
||||||
|
cluster uint32
|
||||||
|
z *ZimReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenient method to return the Article at URL index idx
|
||||||
|
func (z *ZimReader) ArticleAtURLIdx(idx uint32) (*Article, error) {
|
||||||
|
o, err := z.OffsetAtURLIdx(idx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return z.ArticleAt(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the article main page if it exists
|
||||||
|
func (z *ZimReader) MainPage() (*Article, error) {
|
||||||
|
if z.mainPage == 0xffffffff {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return z.ArticleAtURLIdx(z.mainPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the article (Directory) pointed by the offset found in URLpos or Titlepos
|
||||||
|
func (z *ZimReader) ArticleAt(offset uint64) (*Article, error) {
|
||||||
|
a := articlePool.Get().(*Article)
|
||||||
|
err := z.FillArticleAt(a, offset)
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill an article with datas found at offset
|
||||||
|
func (z *ZimReader) FillArticleAt(a *Article, offset uint64) error {
|
||||||
|
a.z = z
|
||||||
|
a.URLPtr = offset
|
||||||
|
|
||||||
|
mimeIdx, err := readInt16(z.bytesRangeAt(offset, offset+2))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't read article %w", err)
|
||||||
|
}
|
||||||
|
a.EntryType = mimeIdx
|
||||||
|
|
||||||
|
// Linktarget or Target Entry
|
||||||
|
if mimeIdx == LinkTargetEntry || mimeIdx == DeletedEntry {
|
||||||
|
// TODO
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := z.bytesRangeAt(offset+3, offset+4)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Namespace = s[0]
|
||||||
|
|
||||||
|
a.cluster, err = readInt32(z.bytesRangeAt(offset+8, offset+8+4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.blob, err = readInt32(z.bytesRangeAt(offset+12, offset+12+4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect
|
||||||
|
if mimeIdx == RedirectEntry {
|
||||||
|
// assume the url + title won't be longer than 2k
|
||||||
|
b, err := z.bytesRangeAt(offset+12, offset+12+2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bbuf := bytes.NewBuffer(b)
|
||||||
|
a.url, err = bbuf.ReadString('\x00')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.url = strings.TrimRight(a.url, "\x00")
|
||||||
|
|
||||||
|
a.Title, err = bbuf.ReadString('\x00')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Title = strings.TrimRight(a.Title, "\x00")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := z.bytesRangeAt(offset+16, offset+16+2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bbuf := bytes.NewBuffer(b)
|
||||||
|
a.url, err = bbuf.ReadString('\x00')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.url = strings.TrimRight(string(a.url), "\x00")
|
||||||
|
|
||||||
|
title, err := bbuf.ReadString('\x00')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
title = strings.TrimRight(string(title), "\x00")
|
||||||
|
// This is a trick to force a copy and avoid retain of the full buffer
|
||||||
|
// mainly for indexing title reasons
|
||||||
|
if len(title) != 0 {
|
||||||
|
a.Title = title[0:1] + title[1:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the uncompressed data associated with this article
|
||||||
|
func (a *Article) Data() ([]byte, error) {
|
||||||
|
// ensure we have data to read
|
||||||
|
if a.EntryType == RedirectEntry || a.EntryType == LinkTargetEntry || a.EntryType == DeletedEntry {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
start, end, err := a.z.clusterOffsetsAtIdx(a.cluster)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s, err := a.z.bytesRangeAt(start, start+1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
compression := uint8(s[0])
|
||||||
|
|
||||||
|
// blob starts at offset, blob ends at offset
|
||||||
|
var bs, be uint32
|
||||||
|
|
||||||
|
// LZMA: 4, Zstandard: 5
|
||||||
|
if compression == 4 || compression == 5 {
|
||||||
|
blobLookup := func() ([]byte, bool) {
|
||||||
|
if v, ok := bcache.Get(a.cluster); ok {
|
||||||
|
b := v.([]byte)
|
||||||
|
return b, ok
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
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 nil, errors.New("Unhandled compression")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Article) MimeType() string {
|
||||||
|
if a.EntryType == RedirectEntry || a.EntryType == LinkTargetEntry || a.EntryType == DeletedEntry {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.z.mimeTypeList[a.EntryType]
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the url prefixed by the namespace
|
||||||
|
func (a *Article) FullURL() string {
|
||||||
|
return string(a.Namespace) + "/" + a.url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Article) String() string {
|
||||||
|
return fmt.Sprintf("Mime: 0x%x URL: [%s], Title: [%s], Cluster: 0x%x Blob: 0x%x",
|
||||||
|
a.EntryType, a.FullURL(), a.Title, a.cluster, a.blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedirectIndex return the redirect index of RedirectEntry type article
|
||||||
|
// return an err if not a redirect entry
|
||||||
|
func (a *Article) RedirectIndex() (uint32, error) {
|
||||||
|
if a.EntryType != RedirectEntry {
|
||||||
|
return 0, errors.New("Not a RedirectEntry")
|
||||||
|
}
|
||||||
|
// We use the cluster to save the redirect index position for RedirectEntry type
|
||||||
|
return a.cluster, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Article) blobOffsetsAtIdx(z *ZimReader) (start, end uint64) {
|
||||||
|
idx := a.blob
|
||||||
|
offset := z.clusterPtrPos + uint64(idx)*8
|
||||||
|
start, err := readInt64(z.bytesRangeAt(offset, offset+8))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offset = z.clusterPtrPos + uint64(idx+1)*8
|
||||||
|
end, _ = readInt64(z.bytesRangeAt(offset, offset+8))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
7
pkg/bundle/oldzim/error.go
Normal file
7
pkg/bundle/oldzim/error.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
)
|
49
pkg/bundle/oldzim/favicon.go
Normal file
49
pkg/bundle/oldzim/favicon.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import "github.com/pkg/errors"
|
||||||
|
|
||||||
|
func (z *ZimReader) Favicon() (*Article, error) {
|
||||||
|
illustration, err := z.getMetadataIllustration()
|
||||||
|
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if illustration != nil {
|
||||||
|
return illustration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaces := []string{"-", "I"}
|
||||||
|
entryNames := []string{"favicon", "favicon.png"}
|
||||||
|
|
||||||
|
for _, ns := range namespaces {
|
||||||
|
for _, en := range entryNames {
|
||||||
|
article, err := z.GetPageNoIndex(ns + "/" + en)
|
||||||
|
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if article != nil {
|
||||||
|
return article, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.WithStack(ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *ZimReader) getMetadataIllustration() (*Article, error) {
|
||||||
|
metadata, err := z.Metadata(MetadataIllustration96x96at2, MetadataIllustration48x48at1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := metadata[MetadataIllustration96x96at2]; exists {
|
||||||
|
return z.GetPageNoIndex("M/" + string(MetadataIllustration96x96at2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := metadata[MetadataIllustration48x48at1]; exists {
|
||||||
|
return z.GetPageNoIndex("M/" + string(MetadataIllustration48x48at1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.WithStack(ErrNotFound)
|
||||||
|
}
|
69
pkg/bundle/oldzim/metadata.go
Normal file
69
pkg/bundle/oldzim/metadata.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MetadataKey string
|
||||||
|
|
||||||
|
// See https://wiki.openzim.org/wiki/Metadata
|
||||||
|
const (
|
||||||
|
MetadataName MetadataKey = "Name"
|
||||||
|
MetadataTitle MetadataKey = "Title"
|
||||||
|
MetadataDescription MetadataKey = "Description"
|
||||||
|
MetadataLongDescription MetadataKey = "LongDescription"
|
||||||
|
MetadataCreator MetadataKey = "Creator"
|
||||||
|
MetadataTags MetadataKey = "Tags"
|
||||||
|
MetadataDate MetadataKey = "Date"
|
||||||
|
MetadataPublisher MetadataKey = "Publisher"
|
||||||
|
MetadataFlavour MetadataKey = "Flavour"
|
||||||
|
MetadataSource MetadataKey = "Source"
|
||||||
|
MetadataLanguage MetadataKey = "Language"
|
||||||
|
MetadataIllustration48x48at1 MetadataKey = "Illustration_48x48@1"
|
||||||
|
MetadataIllustration96x96at2 MetadataKey = "Illustration_96x96@2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var knownKeys = []MetadataKey{
|
||||||
|
MetadataName,
|
||||||
|
MetadataTitle,
|
||||||
|
MetadataDescription,
|
||||||
|
MetadataLongDescription,
|
||||||
|
MetadataCreator,
|
||||||
|
MetadataPublisher,
|
||||||
|
MetadataLanguage,
|
||||||
|
MetadataTags,
|
||||||
|
MetadataDate,
|
||||||
|
MetadataFlavour,
|
||||||
|
MetadataSource,
|
||||||
|
MetadataIllustration48x48at1,
|
||||||
|
MetadataIllustration96x96at2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns a copy of the internal metadata map of the ZIM file.
|
||||||
|
func (z *ZimReader) Metadata(keys ...MetadataKey) (map[MetadataKey]string, error) {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
keys = knownKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := make(map[MetadataKey]string)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
article, err := z.GetPageNoIndex("M/" + string(key))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := article.Data()
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata[key] = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
|
}
|
43
pkg/bundle/oldzim/tools.go
Normal file
43
pkg/bundle/oldzim/tools.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// read a little endian uint64
|
||||||
|
func readInt64(b []byte, err error) (v uint64, aerr error) {
|
||||||
|
if err != nil {
|
||||||
|
aerr = err
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(b)
|
||||||
|
aerr = binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a little endian uint32
|
||||||
|
func readInt32(b []byte, err error) (v uint32, aerr error) {
|
||||||
|
if err != nil {
|
||||||
|
aerr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(b)
|
||||||
|
aerr = binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a little endian uint32
|
||||||
|
func readInt16(b []byte, err error) (v uint16, aerr error) {
|
||||||
|
if err != nil {
|
||||||
|
aerr = err
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(b)
|
||||||
|
aerr = binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
23
pkg/bundle/oldzim/xz_reader.go
Normal file
23
pkg/bundle/oldzim/xz_reader.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ulikunitz/xz"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XZReader struct {
|
||||||
|
*xz.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXZReader(r io.Reader) (*XZReader, error) {
|
||||||
|
dec, err := xz.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &XZReader{dec}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xr *XZReader) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
321
pkg/bundle/oldzim/zim.go
Normal file
321
pkg/bundle/oldzim/zim.go
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
zimHeader = 72173914
|
||||||
|
)
|
||||||
|
|
||||||
|
// ZimReader keep tracks of everything related to ZIM reading
|
||||||
|
type ZimReader struct {
|
||||||
|
f *os.File
|
||||||
|
UUID uint32
|
||||||
|
ArticleCount uint32
|
||||||
|
clusterCount uint32
|
||||||
|
urlPtrPos uint64
|
||||||
|
titlePtrPos uint64
|
||||||
|
clusterPtrPos uint64
|
||||||
|
mimeListPos uint64
|
||||||
|
mainPage uint32
|
||||||
|
layoutPage uint32
|
||||||
|
mimeTypeList []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new zim reader
|
||||||
|
func NewReader(path string) (*ZimReader, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
z := ZimReader{f: f, mainPage: 0xffffffff, layoutPage: 0xffffffff}
|
||||||
|
|
||||||
|
articlePool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(Article)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// keep 4 latest uncompressed blobs, around 1M per blob
|
||||||
|
bcache, _ = lru.New[any, any](5)
|
||||||
|
|
||||||
|
err = z.readFileHeaders()
|
||||||
|
return &z, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an ordered list of mime types present in the ZIM file
|
||||||
|
func (z *ZimReader) MimeTypes() []string {
|
||||||
|
if len(z.mimeTypeList) != 0 {
|
||||||
|
return z.mimeTypeList
|
||||||
|
}
|
||||||
|
|
||||||
|
var s []string
|
||||||
|
// assume mime list fit in 2k
|
||||||
|
b, err := z.bytesRangeAt(z.mimeListPos, z.mimeListPos+2048)
|
||||||
|
if err != nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
bbuf := bytes.NewBuffer(b)
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := bbuf.ReadBytes('\x00')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
// a line of 1 is a line containing only \x00 and it's the marker for the
|
||||||
|
// end of mime types list
|
||||||
|
if len(line) == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s = append(s, strings.TrimRight(string(line), "\x00"))
|
||||||
|
}
|
||||||
|
z.mimeTypeList = s
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// list all articles, using url index, contained in a zim file
|
||||||
|
// note that this is a slow implementation, a real iterator is faster
|
||||||
|
// you are not suppose to use this method on big zim files, use indexes
|
||||||
|
func (z *ZimReader) ListArticles() <-chan *Article {
|
||||||
|
ch := make(chan *Article, 10)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var idx uint32
|
||||||
|
// starting at 1 to avoid "con" entry
|
||||||
|
var start uint32 = 1
|
||||||
|
|
||||||
|
for idx = start; idx < z.ArticleCount; idx++ {
|
||||||
|
art, err := z.ArticleAtURLIdx(idx)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if art == nil {
|
||||||
|
// TODO: deal with redirect continue
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ch <- art
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// list all title pointer, Titles by position contained in a zim file
|
||||||
|
// Titles are pointers to URLpos index, useful for indexing cause smaller to store: uint32
|
||||||
|
// note that this is a slow implementation, a real iterator is faster
|
||||||
|
// you are not suppose to use this method on big zim files prefer ListTitlesPtrIterator to build your index
|
||||||
|
func (z *ZimReader) ListTitlesPtr() <-chan uint32 {
|
||||||
|
ch := make(chan uint32, 10)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var pos uint64
|
||||||
|
var count uint32
|
||||||
|
|
||||||
|
for pos = z.titlePtrPos; count < z.ArticleCount; pos += 4 {
|
||||||
|
idx, err := readInt32(z.bytesRangeAt(pos, pos+4))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ch <- idx
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// list all title pointer, Titles by position contained in a zim file
|
||||||
|
// Titles are pointers to URLpos index, usefull for indexing cause smaller to store: uint32
|
||||||
|
func (z *ZimReader) ListTitlesPtrIterator(cb func(uint32)) {
|
||||||
|
var count uint32
|
||||||
|
for pos := z.titlePtrPos; count < z.ArticleCount; pos += 4 {
|
||||||
|
idx, err := readInt32(z.bytesRangeAt(pos, pos+4))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cb(idx)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the article at the exact url not using any index
|
||||||
|
func (z *ZimReader) GetPageNoIndex(url string) (*Article, error) {
|
||||||
|
// starting at 1 to avoid "con" entry
|
||||||
|
var start uint32
|
||||||
|
stop := z.ArticleCount
|
||||||
|
|
||||||
|
a := new(Article)
|
||||||
|
|
||||||
|
for {
|
||||||
|
pos := (start + stop) / 2
|
||||||
|
|
||||||
|
offset, err := z.OffsetAtURLIdx(pos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = z.FillArticleAt(a, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.FullURL() == url {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.FullURL() > url {
|
||||||
|
stop = pos
|
||||||
|
} else {
|
||||||
|
start = pos
|
||||||
|
}
|
||||||
|
if stop-start == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil, errors.WithStack(ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the offset pointing to Article at pos in the URL idx
|
||||||
|
func (z *ZimReader) OffsetAtURLIdx(idx uint32) (uint64, error) {
|
||||||
|
offset := z.urlPtrPos + uint64(idx)*8
|
||||||
|
return readInt64(z.bytesRangeAt(offset, offset+8))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close & cleanup the zimreader
|
||||||
|
func (z *ZimReader) Close() error {
|
||||||
|
return z.f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *ZimReader) String() string {
|
||||||
|
fi, err := z.f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return "corrupted zim"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Size: %d, ArticleCount: %d urlPtrPos: 0x%x titlePtrPos: 0x%x mimeListPos: 0x%x clusterPtrPos: 0x%x\nMimeTypes: %v",
|
||||||
|
fi.Size(), z.ArticleCount, z.urlPtrPos, z.titlePtrPos, z.mimeListPos, z.clusterPtrPos, z.MimeTypes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBytesRangeAt returns bytes from start to end
|
||||||
|
// it's needed to abstract mmap usages rather than read directly on the mmap slices
|
||||||
|
func (z *ZimReader) bytesRangeAt(start, end uint64) ([]byte, error) {
|
||||||
|
buf := make([]byte, end-start)
|
||||||
|
n, err := z.f.ReadAt(buf, int64(start))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read bytes %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != int(end-start) {
|
||||||
|
return nil, errors.New("can't read enough bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate the ZimReader structs with headers
|
||||||
|
func (z *ZimReader) readFileHeaders() error {
|
||||||
|
// checking for file type
|
||||||
|
v, err := readInt32(z.bytesRangeAt(0, 0+4))
|
||||||
|
if err != nil || v != zimHeader {
|
||||||
|
return errors.New("not a ZIM file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// checking for version
|
||||||
|
v, err = readInt32(z.bytesRangeAt(4, 4+4))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not read file version")
|
||||||
|
}
|
||||||
|
|
||||||
|
// checking for articles count
|
||||||
|
v, err = readInt32(z.bytesRangeAt(8, 16))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.UUID = v
|
||||||
|
|
||||||
|
// checking for articles count
|
||||||
|
v, err = readInt32(z.bytesRangeAt(24, 24+4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.ArticleCount = v
|
||||||
|
|
||||||
|
// checking for cluster count
|
||||||
|
v, err = readInt32(z.bytesRangeAt(28, 28+4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.clusterCount = v
|
||||||
|
|
||||||
|
// checking for urlPtrPos
|
||||||
|
vb, err := readInt64(z.bytesRangeAt(32, 32+8))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.urlPtrPos = vb
|
||||||
|
|
||||||
|
// checking for titlePtrPos
|
||||||
|
vb, err = readInt64(z.bytesRangeAt(40, 40+8))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.titlePtrPos = vb
|
||||||
|
|
||||||
|
// checking for clusterPtrPos
|
||||||
|
vb, err = readInt64(z.bytesRangeAt(48, 48+8))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.clusterPtrPos = vb
|
||||||
|
|
||||||
|
// checking for mimeListPos
|
||||||
|
vb, err = readInt64(z.bytesRangeAt(56, 56+8))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.mimeListPos = vb
|
||||||
|
|
||||||
|
// checking for mainPage
|
||||||
|
v, err = readInt32(z.bytesRangeAt(64, 64+4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.mainPage = v
|
||||||
|
|
||||||
|
// checking for layoutPage
|
||||||
|
v, err = readInt32(z.bytesRangeAt(68, 68+4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
z.layoutPage = v
|
||||||
|
|
||||||
|
spew.Dump(z)
|
||||||
|
|
||||||
|
z.MimeTypes()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return start and end offsets for cluster at index idx
|
||||||
|
func (z *ZimReader) clusterOffsetsAtIdx(idx uint32) (start, end uint64, err error) {
|
||||||
|
offset := z.clusterPtrPos + (uint64(idx) * 8)
|
||||||
|
start, err = readInt64(z.bytesRangeAt(offset, offset+8))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
offset = z.clusterPtrPos + (uint64(idx+1) * 8)
|
||||||
|
end, err = readInt64(z.bytesRangeAt(offset, offset+8))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end--
|
||||||
|
return
|
||||||
|
}
|
153
pkg/bundle/oldzim/zim_test.go
Normal file
153
pkg/bundle/oldzim/zim_test.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
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)
|
||||||
|
}
|
26
pkg/bundle/oldzim/zstd_reader.go
Normal file
26
pkg/bundle/oldzim/zstd_reader.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZstdReader struct {
|
||||||
|
*zstd.Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZstdReader(r io.Reader) (*ZstdReader, error) {
|
||||||
|
dec, err := zstd.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read from zstd %w", err)
|
||||||
|
}
|
||||||
|
return &ZstdReader{dec}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zr *ZstdReader) Close() error {
|
||||||
|
zr.Decoder.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
233
pkg/bundle/zim/content_entry.go
Normal file
233
pkg/bundle/zim/content_entry.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
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
|
||||||
|
}
|
150
pkg/bundle/zim/entry.go
Normal file
150
pkg/bundle/zim/entry.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
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
|
||||||
|
}
|
46
pkg/bundle/zim/entry_iterator.go
Normal file
46
pkg/bundle/zim/entry_iterator.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
|
}
|
10
pkg/bundle/zim/error.go
Normal file
10
pkg/bundle/zim/error.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package zim
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidEntryIndex = errors.New("invalid entry index")
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
ErrInvalidRedirect = errors.New("invalid redirect")
|
||||||
|
ErrCompressionAlgorithmNotSupported = errors.New("compression algorithm not supported")
|
||||||
|
)
|
38
pkg/bundle/zim/option.go
Normal file
38
pkg/bundle/zim/option.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
522
pkg/bundle/zim/reader.go
Normal file
522
pkg/bundle/zim/reader.go
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
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
|
||||||
|
}
|
83
pkg/bundle/zim/reader_test.go
Normal file
83
pkg/bundle/zim/reader_test.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
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
Normal file
BIN
pkg/bundle/zim/testdata/cadoles.zim
vendored
Normal file
Binary file not shown.
BIN
pkg/bundle/zim/testdata/wikibooks_af_all_maxi_2023-06.zim
vendored
Normal file
BIN
pkg/bundle/zim/testdata/wikibooks_af_all_maxi_2023-06.zim
vendored
Normal file
Binary file not shown.
52
pkg/bundle/zim/util.go
Normal file
52
pkg/bundle/zim/util.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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
|
||||||
|
}
|
447
pkg/bundle/zim_bundle.go
Normal file
447
pkg/bundle/zim_bundle.go
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
package bundle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bundle/zim"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZimBundle struct {
|
||||||
|
archivePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
ctx := logger.With(
|
||||||
|
context.Background(),
|
||||||
|
logger.F("filename", filename),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.Debug(ctx, "opening file")
|
||||||
|
|
||||||
|
switch filename {
|
||||||
|
case "manifest.yml":
|
||||||
|
return b.renderFakeManifest(ctx)
|
||||||
|
case "server/main.js":
|
||||||
|
return b.renderFakeServerMain(ctx)
|
||||||
|
case "public":
|
||||||
|
return b.renderDirectory(ctx, filename)
|
||||||
|
case "public/index.html":
|
||||||
|
return b.redirectToMainPage(ctx, filename)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return b.renderURL(ctx, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) Dir(dirname string) ([]os.FileInfo, error) {
|
||||||
|
reader, err := b.openArchive()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
files := make([]os.FileInfo, 0)
|
||||||
|
// ctx := context.Background()
|
||||||
|
|
||||||
|
// for _, f := range reader.File {
|
||||||
|
// if !strings.HasPrefix(f.Name, dirname) {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// relPath, err := filepath.Rel(dirname, f.Name)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, errors.Wrap(err, "could not get relative path")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// logger.Debug(
|
||||||
|
// ctx, "checking file prefix",
|
||||||
|
// logger.F("dirname", dirname),
|
||||||
|
// logger.F("filename", f.Name),
|
||||||
|
// logger.F("relpath", relPath),
|
||||||
|
// )
|
||||||
|
|
||||||
|
// if relPath == filepath.Base(f.Name) {
|
||||||
|
// files = append(files, f.FileInfo())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) renderFakeManifest(ctx context.Context) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
reader, err := b.openArchive()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
metadata, err := reader.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := map[string]any{}
|
||||||
|
|
||||||
|
manifest["version"] = "0.0.0"
|
||||||
|
|
||||||
|
if name, exists := metadata[zim.MetadataName]; exists {
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"_", "",
|
||||||
|
" ", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest["id"] = strings.ToLower(replacer.Replace(name)) + ".zim.edge.app"
|
||||||
|
} else {
|
||||||
|
manifest["id"] = strconv.FormatUint(uint64(reader.UUID), 10) + ".zim.edge.app"
|
||||||
|
}
|
||||||
|
|
||||||
|
if title, exists := metadata[zim.MetadataTitle]; exists {
|
||||||
|
manifest["title"] = title
|
||||||
|
} else {
|
||||||
|
manifest["title"] = "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if description, exists := metadata[zim.MetadataDescription]; exists {
|
||||||
|
manifest["description"] = description
|
||||||
|
}
|
||||||
|
|
||||||
|
favicon, err := reader.Favicon()
|
||||||
|
if err != nil && !errors.Is(err, zim.ErrNotFound) {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if favicon != nil {
|
||||||
|
manifestMeta, exists := manifest["metadata"].(map[string]any)
|
||||||
|
if !exists {
|
||||||
|
manifestMeta = make(map[string]any)
|
||||||
|
manifest["metadata"] = manifestMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
paths, exists := manifestMeta["paths"].(map[string]any)
|
||||||
|
if !exists {
|
||||||
|
paths = make(map[string]any)
|
||||||
|
manifestMeta["paths"] = paths
|
||||||
|
}
|
||||||
|
|
||||||
|
paths["icon"] = "/" + favicon.FullURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := yaml.Marshal(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stat := &zimFileInfo{
|
||||||
|
isDir: false,
|
||||||
|
modTime: time.Time{},
|
||||||
|
mode: 0,
|
||||||
|
name: "manifest.yml",
|
||||||
|
size: int64(len(data)),
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
file := ioutil.NopCloser(buf)
|
||||||
|
|
||||||
|
return file, stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) renderFakeServerMain(ctx context.Context) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
stat := &zimFileInfo{
|
||||||
|
isDir: false,
|
||||||
|
modTime: time.Time{},
|
||||||
|
mode: 0,
|
||||||
|
name: "server/main.js",
|
||||||
|
size: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
file := ioutil.NopCloser(buf)
|
||||||
|
|
||||||
|
return file, stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) renderURL(ctx context.Context, url string) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
zr, err := b.openArchive()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := zr.Close(); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
filename := filepath.Base(url)
|
||||||
|
url = strings.TrimPrefix(url, "public/")
|
||||||
|
|
||||||
|
article, err := zr.GetPageNoIndex(url)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, zim.ErrNotFound) {
|
||||||
|
return nil, nil, errors.WithStack(fs.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if article.EntryType == zim.RedirectEntry {
|
||||||
|
redirectIndex, err := article.RedirectIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ra, err := zr.ArticleAtURLIdx(redirectIndex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.renderRedirect(ctx, filename, ra.FullURL())
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := article.Data()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType := article.MimeType()
|
||||||
|
if mimeType == "text/html" {
|
||||||
|
injected, err := b.injectEdgeScriptTag(data)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not inject edge script", logger.E(errors.WithStack(err)))
|
||||||
|
} else {
|
||||||
|
data = injected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zimFile := &zimFile{
|
||||||
|
fileInfo: &zimFileInfo{
|
||||||
|
isDir: false,
|
||||||
|
modTime: time.Time{},
|
||||||
|
mode: 0,
|
||||||
|
name: filename,
|
||||||
|
size: int64(len(data)),
|
||||||
|
},
|
||||||
|
buff: bytes.NewBuffer(data),
|
||||||
|
}
|
||||||
|
|
||||||
|
return zimFile, zimFile.fileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) renderDirectory(ctx context.Context, filename string) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
zr, err := b.openArchive()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := zr.Close(); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
zimFile := &zimFile{
|
||||||
|
fileInfo: &zimFileInfo{
|
||||||
|
isDir: true,
|
||||||
|
modTime: time.Time{},
|
||||||
|
mode: 0,
|
||||||
|
name: filename,
|
||||||
|
size: 0,
|
||||||
|
},
|
||||||
|
buff: bytes.NewBuffer(nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
return zimFile, zimFile.fileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) renderRedirect(ctx context.Context, filename string, to string) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
logger.Debug(ctx, "rendering redirect", logger.F("url", to))
|
||||||
|
|
||||||
|
data := fmt.Sprintf(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0; url=/%s" />
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
|
`, to)
|
||||||
|
|
||||||
|
stat := &zimFileInfo{
|
||||||
|
isDir: false,
|
||||||
|
modTime: time.Time{},
|
||||||
|
mode: 0,
|
||||||
|
name: filename,
|
||||||
|
size: int64(len(data)),
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer([]byte(data))
|
||||||
|
reader := ioutil.NopCloser(buf)
|
||||||
|
|
||||||
|
return reader, stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) redirectToMainPage(ctx context.Context, filename string) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
zr, err := b.openArchive()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := zr.Close(); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
main, err := zr.MainPage()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.renderRedirect(ctx, filename, main.FullURL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) injectEdgeScriptTag(data []byte) ([]byte, error) {
|
||||||
|
buff := bytes.NewBuffer(data)
|
||||||
|
doc, err := html.Parse(buff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var f func(*html.Node) bool
|
||||||
|
f = func(n *html.Node) bool {
|
||||||
|
if n.Type == html.ElementNode && n.Data == "head" {
|
||||||
|
script := &html.Node{
|
||||||
|
Type: html.ElementNode,
|
||||||
|
Data: "script",
|
||||||
|
Attr: []html.Attribute{
|
||||||
|
{
|
||||||
|
Key: "src",
|
||||||
|
Val: "/edge/sdk/client.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
n.AppendChild(script)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if keepWalking := f(c); !keepWalking {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
f(doc)
|
||||||
|
|
||||||
|
buff.Reset()
|
||||||
|
|
||||||
|
if err := html.Render(buff, doc); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buff.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ZimBundle) openArchive() (*zim.ZimReader, error) {
|
||||||
|
zm, err := zim.NewReader(b.archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not open '%v'", b.archivePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZimBundle(archivePath string) *ZimBundle {
|
||||||
|
return &ZimBundle{
|
||||||
|
archivePath: archivePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type zimFile struct {
|
||||||
|
fileInfo *zimFileInfo
|
||||||
|
buff *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements fs.File.
|
||||||
|
func (f *zimFile) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements fs.File.
|
||||||
|
func (f *zimFile) Read(d []byte) (int, error) {
|
||||||
|
return f.buff.Read(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat implements fs.File.
|
||||||
|
func (f *zimFile) Stat() (fs.FileInfo, error) {
|
||||||
|
return f.fileInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fs.File = &zimFile{}
|
||||||
|
|
||||||
|
type zimFileInfo struct {
|
||||||
|
isDir bool
|
||||||
|
modTime time.Time
|
||||||
|
mode fs.FileMode
|
||||||
|
name string
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir implements fs.FileInfo.
|
||||||
|
func (i *zimFileInfo) IsDir() bool {
|
||||||
|
return i.isDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime implements fs.FileInfo.
|
||||||
|
func (i *zimFileInfo) ModTime() time.Time {
|
||||||
|
return i.modTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode implements fs.FileInfo.
|
||||||
|
func (i *zimFileInfo) Mode() fs.FileMode {
|
||||||
|
return i.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements fs.FileInfo.
|
||||||
|
func (i *zimFileInfo) Name() string {
|
||||||
|
return i.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size implements fs.FileInfo.
|
||||||
|
func (i *zimFileInfo) Size() int64 {
|
||||||
|
return i.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sys implements fs.FileInfo.
|
||||||
|
func (*zimFileInfo) Sys() any {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fs.FileInfo = &zimFileInfo{}
|
@ -97,6 +97,10 @@ 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,6 +18,7 @@ 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 {
|
||||||
@ -34,7 +35,8 @@ func defaultHandlerOptions() *HandlerOptions {
|
|||||||
HTTPClient: &http.Client{
|
HTTPClient: &http.Client{
|
||||||
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,3 +77,9 @@ 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,5 +1,6 @@
|
|||||||
package auth
|
package jwtutil
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var ErrUnauthenticated = errors.New("unauthenticated")
|
var ErrUnauthenticated = errors.New("unauthenticated")
|
||||||
|
var ErrNoKeySet = errors.New("no keyset")
|
71
pkg/jwtutil/io.go
Normal file
71
pkg/jwtutil/io.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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
|
||||||
|
}
|
77
pkg/jwtutil/key.go
Normal file
77
pkg/jwtutil/key.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
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
|
||||||
|
}
|
119
pkg/jwtutil/request.go
Normal file
119
pkg/jwtutil/request.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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
|
||||||
|
}
|
53
pkg/jwtutil/token.go
Normal file
53
pkg/jwtutil/token.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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,35 +0,0 @@
|
|||||||
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,6 +7,7 @@ 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"
|
||||||
@ -30,12 +31,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LocalHandler) initRouter(prefix string) {
|
func (h *LocalHandler) initRouter(prefix string) {
|
||||||
@ -112,7 +113,7 @@ func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
account.Claims[auth.ClaimIssuer] = "local"
|
account.Claims[auth.ClaimIssuer] = "local"
|
||||||
|
|
||||||
token, err := generateSignedToken(h.algo, h.key, account.Claims)
|
token, err := jwtutil.SignedToken(h.key, h.signingAlgorithm, 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)
|
||||||
@ -181,18 +182,18 @@ func (h *LocalHandler) authenticate(username, password string) (*LocalAccount, e
|
|||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocalHandler(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...LocalHandlerOptionFunc) *LocalHandler {
|
func NewLocalHandler(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.initRouter(opts.RoutePrefix)
|
handler.initRouter(opts.RoutePrefix)
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
117
pkg/module/auth/middleware/anonymous_user.go
Normal file
117
pkg/module/auth/middleware/anonymous_user.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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
|
||||||
|
}
|
57
pkg/module/auth/middleware/options.go
Normal file
57
pkg/module/auth/middleware/options.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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,12 +5,17 @@ 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"
|
||||||
@ -21,8 +26,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
server *app.Server
|
server *app.Server
|
||||||
getClaims GetClaimsFunc
|
getClaimFn GetClaimFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) Name() string {
|
func (m *Module) Name() string {
|
||||||
@ -68,9 +73,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.getClaims(ctx, req, claimName)
|
claim, err := m.getClaimFn(ctx, req, claimName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrUnauthenticated) {
|
if errors.Is(err, jwtutil.ErrUnauthenticated) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,11 +83,7 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(claim) == 0 || claim[0] == "" {
|
return rt.ToValue(claim)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return rt.ToValue(claim[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
||||||
@ -93,8 +94,8 @@ func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
|||||||
|
|
||||||
return func(server *app.Server) app.ServerModule {
|
return func(server *app.Server) app.ServerModule {
|
||||||
return &Module{
|
return &Module{
|
||||||
server: server,
|
server: server,
|
||||||
getClaims: opt.GetClaims,
|
getClaimFn: opt.GetClaim,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ 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"
|
||||||
@ -130,7 +131,7 @@ func getDummyKey() jwk.Key {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDummyKeySet(key jwk.Key) GetKeySetFunc {
|
func getDummyKeySet(key jwk.Key) jwtutil.GetKeySetFunc {
|
||||||
return func() (jwk.Set, error) {
|
return func() (jwk.Set, error) {
|
||||||
set := jwk.NewSet()
|
set := jwk.NewSet()
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ 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"
|
||||||
@ -12,39 +13,39 @@ import (
|
|||||||
type MountFunc func(r chi.Router)
|
type MountFunc func(r chi.Router)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
getClaims GetClaimsFunc
|
getClaim GetClaimFunc
|
||||||
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()
|
||||||
|
|
||||||
claims, err := h.getClaims(ctx, r, h.profileClaims...)
|
profile := make(map[string]any)
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrUnauthenticated) {
|
for _, name := range h.profileClaims {
|
||||||
|
value, err := h.getClaim(ctx, r, name)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, jwtutil.ErrUnauthenticated) {
|
||||||
|
api.ErrorResponse(
|
||||||
|
w, http.StatusUnauthorized,
|
||||||
|
api.ErrCodeUnauthorized,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(ctx, "could not retrieve claims", logger.E(errors.WithStack(err)))
|
||||||
api.ErrorResponse(
|
api.ErrorResponse(
|
||||||
w, http.StatusUnauthorized,
|
w, http.StatusInternalServerError,
|
||||||
api.ErrCodeUnauthorized,
|
api.ErrCodeUnknownError,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Error(ctx, "could not retrieve claims", logger.E(errors.WithStack(err)))
|
profile[name] = value
|
||||||
api.ErrorResponse(
|
|
||||||
w, http.StatusInternalServerError,
|
|
||||||
api.ErrCodeUnknownError,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
profile := make(map[string]any)
|
|
||||||
|
|
||||||
for idx, cl := range h.profileClaims {
|
|
||||||
profile[cl] = claims[idx]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
@ -62,7 +63,7 @@ func Mount(authHandler http.Handler, funcs ...OptionFunc) MountFunc {
|
|||||||
|
|
||||||
handler := &Handler{
|
handler := &Handler{
|
||||||
profileClaims: opt.ProfileClaims,
|
profileClaims: opt.ProfileClaims,
|
||||||
getClaims: opt.GetClaims,
|
getClaim: opt.GetClaim,
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(r chi.Router) {
|
return func(r chi.Router) {
|
||||||
|
@ -2,15 +2,17 @@ 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 GetClaimsFunc func(ctx context.Context, r *http.Request, claims ...string) ([]string, error)
|
type GetClaimFunc func(ctx context.Context, r *http.Request, name string) (string, error)
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
GetClaims GetClaimsFunc
|
GetClaim GetClaimFunc
|
||||||
ProfileClaims []string
|
ProfileClaims []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +20,7 @@ type OptionFunc func(*Option)
|
|||||||
|
|
||||||
func defaultOptions() *Option {
|
func defaultOptions() *Option {
|
||||||
return &Option{
|
return &Option{
|
||||||
GetClaims: dummyGetClaims,
|
GetClaim: dummyGetClaim,
|
||||||
ProfileClaims: []string{
|
ProfileClaims: []string{
|
||||||
ClaimSubject,
|
ClaimSubject,
|
||||||
ClaimIssuer,
|
ClaimIssuer,
|
||||||
@ -30,13 +32,13 @@ func defaultOptions() *Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dummyGetClaims(ctx context.Context, r *http.Request, claims ...string) ([]string, error) {
|
func dummyGetClaim(ctx context.Context, r *http.Request, name string) (string, error) {
|
||||||
return nil, errors.Errorf("dummy getclaim func cannot retrieve claims '%s'", claims)
|
return "", errors.Errorf("dummy getclaim func cannot retrieve claim '%s'", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithGetClaims(fn GetClaimsFunc) OptionFunc {
|
func WithGetClaims(fn GetClaimFunc) OptionFunc {
|
||||||
return func(o *Option) {
|
return func(o *Option) {
|
||||||
o.GetClaims = fn
|
o.GetClaim = fn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,3 +47,34 @@ 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 (
|
||||||
"io/ioutil"
|
"os"
|
||||||
"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/sqlite"
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/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 := ioutil.ReadFile("testdata/blob.js")
|
data, err := os.ReadFile("testdata/blob.js")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -5,18 +5,19 @@ 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 ValueType = "*"
|
AnyType share.ValueType = "*"
|
||||||
AnyName string = "*"
|
AnyName string = "*"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
appID app.ID
|
appID app.ID
|
||||||
repository Repository
|
store share.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) Name() string {
|
func (m *Module) Name() string {
|
||||||
@ -48,19 +49,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", TypeText); err != nil {
|
if err := export.Set("TYPE_TEXT", share.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", TypeNumber); err != nil {
|
if err := export.Set("TYPE_NUMBER", share.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", TypeBool); err != nil {
|
if err := export.Set("TYPE_BOOL", share.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", TypePath); err != nil {
|
if err := export.Set("TYPE_PATH", share.TypePath); err != nil {
|
||||||
panic(errors.Wrap(err, "could not set 'TYPE_PATH' property"))
|
panic(errors.Wrap(err, "could not set 'TYPE_PATH' property"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,20 +70,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 []Attribute
|
var attributes []share.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([]Attribute, 0)
|
attributes = make([]share.Attribute, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, attr := range attributes {
|
for _, attr := range attributes {
|
||||||
if err := AssertType(attr.Value(), attr.Type()); err != nil {
|
if err := share.AssertType(attr.Value(), attr.Type()); err != nil {
|
||||||
panic(rt.ToValue(errors.WithStack(err)))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource, err := m.repository.UpdateAttributes(ctx, m.appID, resourceID, attributes...)
|
resource, err := m.store.UpdateAttributes(ctx, m.appID, resourceID, attributes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(rt.ToValue(errors.WithStack(err)))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
@ -101,7 +102,7 @@ func (m *Module) deleteAttributes(call goja.FunctionCall, rt *goja.Runtime) goja
|
|||||||
names = make([]string, 0)
|
names = make([]string, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := m.repository.DeleteAttributes(ctx, m.appID, resourceID, names...)
|
err := m.store.DeleteAttributes(ctx, m.appID, resourceID, names...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(rt.ToValue(errors.WithStack(err)))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
@ -112,23 +113,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([]FindResourcesOptionFunc, 0)
|
funcs := make([]share.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, WithName(name))
|
funcs = append(funcs, share.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, WithType(valueType))
|
funcs = append(funcs, share.WithType(valueType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resources, err := m.repository.FindResources(ctx, funcs...)
|
resources, err := m.store.FindResources(ctx, funcs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(rt.ToValue(errors.WithStack(err)))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
@ -140,7 +141,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.repository.DeleteResource(ctx, m.appID, resourceID)
|
err := m.store.DeleteResource(ctx, m.appID, resourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(rt.ToValue(errors.WithStack(err)))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
@ -148,29 +149,29 @@ func (m *Module) deleteResource(call goja.FunctionCall, rt *goja.Runtime) goja.V
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleFactory(appID app.ID, repository Repository) app.ServerModuleFactory {
|
func ModuleFactory(appID app.ID, store share.Store) app.ServerModuleFactory {
|
||||||
return func(server *app.Server) app.ServerModule {
|
return func(server *app.Server) app.ServerModule {
|
||||||
return &Module{
|
return &Module{
|
||||||
appID: appID,
|
appID: appID,
|
||||||
repository: repository,
|
store: store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertResourceID(v goja.Value, r *goja.Runtime) ResourceID {
|
func assertResourceID(v goja.Value, r *goja.Runtime) share.ResourceID {
|
||||||
value := v.Export()
|
value := v.Export()
|
||||||
switch typ := value.(type) {
|
switch typ := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
return ResourceID(typ)
|
return share.ResourceID(typ)
|
||||||
case ResourceID:
|
case share.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) []Attribute {
|
func assertAttributes(values []goja.Value, r *goja.Runtime) []share.Attribute {
|
||||||
attributes := make([]Attribute, len(values))
|
attributes := make([]share.Attribute, len(values))
|
||||||
|
|
||||||
for idx, val := range values {
|
for idx, val := range values {
|
||||||
export := val.Export()
|
export := val.Export()
|
||||||
@ -195,12 +196,12 @@ func assertAttributes(values []goja.Value, r *goja.Runtime) []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 ValueType
|
var valueType share.ValueType
|
||||||
switch typ := rawType.(type) {
|
switch typ := rawType.(type) {
|
||||||
case ValueType:
|
case share.ValueType:
|
||||||
valueType = typ
|
valueType = typ
|
||||||
case string:
|
case string:
|
||||||
valueType = ValueType(typ)
|
valueType = share.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)))
|
||||||
@ -211,7 +212,7 @@ func assertAttributes(values []goja.Value, r *goja.Runtime) []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] = NewBaseAttribute(
|
attributes[idx] = share.NewBaseAttribute(
|
||||||
name,
|
name,
|
||||||
valueType,
|
valueType,
|
||||||
value,
|
value,
|
||||||
@ -232,12 +233,12 @@ func assertStrings(values []goja.Value, r *goja.Runtime) []string {
|
|||||||
return strings
|
return strings
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertValueType(v goja.Value, r *goja.Runtime) ValueType {
|
func assertValueType(v goja.Value, r *goja.Runtime) share.ValueType {
|
||||||
value := v.Export()
|
value := v.Export()
|
||||||
switch typ := value.(type) {
|
switch typ := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
return ValueType(typ)
|
return share.ValueType(typ)
|
||||||
case ValueType:
|
case share.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)))
|
||||||
@ -245,7 +246,7 @@ func assertValueType(v goja.Value, r *goja.Runtime) ValueType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type gojaResource struct {
|
type gojaResource struct {
|
||||||
ID ResourceID `goja:"id" json:"id"`
|
ID share.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"`
|
||||||
}
|
}
|
||||||
@ -254,7 +255,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 := HasAttribute(toResource(r), name, valueType)
|
hasAttr := share.HasAttribute(toResource(r), name, valueType)
|
||||||
|
|
||||||
return rt.ToValue(hasAttr)
|
return rt.ToValue(hasAttr)
|
||||||
}
|
}
|
||||||
@ -268,7 +269,7 @@ func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
|||||||
defaultValue = call.Argument(2).Export()
|
defaultValue = call.Argument(2).Export()
|
||||||
}
|
}
|
||||||
|
|
||||||
attr := GetAttribute(toResource(r), name, valueType)
|
attr := share.GetAttribute(toResource(r), name, valueType)
|
||||||
|
|
||||||
if attr == nil {
|
if attr == nil {
|
||||||
return rt.ToValue(defaultValue)
|
return rt.ToValue(defaultValue)
|
||||||
@ -278,14 +279,14 @@ func (r *gojaResource) Get(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
|||||||
}
|
}
|
||||||
|
|
||||||
type gojaAttribute struct {
|
type gojaAttribute struct {
|
||||||
Name string `goja:"name" json:"name"`
|
Name string `goja:"name" json:"name"`
|
||||||
Type ValueType `goja:"type" json:"type"`
|
Type share.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 Resource) *gojaResource {
|
func toGojaResource(res share.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() {
|
||||||
@ -305,7 +306,7 @@ func toGojaResource(res Resource) *gojaResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toGojaResources(resources []Resource) []*gojaResource {
|
func toGojaResources(resources []share.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)
|
||||||
@ -313,19 +314,19 @@ func toGojaResources(resources []Resource) []*gojaResource {
|
|||||||
return gojaResources
|
return gojaResources
|
||||||
}
|
}
|
||||||
|
|
||||||
func toResource(res *gojaResource) Resource {
|
func toResource(res *gojaResource) share.Resource {
|
||||||
return NewBaseResource(
|
return share.NewBaseResource(
|
||||||
res.Origin,
|
res.Origin,
|
||||||
res.ID,
|
res.ID,
|
||||||
toAttributes(res.Attributes)...,
|
toAttributes(res.Attributes)...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAttributes(gojaAttributes []*gojaAttribute) []Attribute {
|
func toAttributes(gojaAttributes []*gojaAttribute) []share.Attribute {
|
||||||
attributes := make([]Attribute, len(gojaAttributes))
|
attributes := make([]share.Attribute, len(gojaAttributes))
|
||||||
|
|
||||||
for idx, gojaAttr := range gojaAttributes {
|
for idx, gojaAttr := range gojaAttributes {
|
||||||
attr := NewBaseAttribute(
|
attr := share.NewBaseAttribute(
|
||||||
gojaAttr.Name,
|
gojaAttr.Name,
|
||||||
gojaAttr.Type,
|
gojaAttr.Type,
|
||||||
gojaAttr.Value,
|
gojaAttr.Value,
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
package testsuite
|
package share
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/fs"
|
"os"
|
||||||
"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/module/share"
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||||
"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, newRepo NewTestRepoFunc) {
|
func TestModule(t *testing.T) {
|
||||||
logger.SetLevel(logger.LevelDebug)
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
|
||||||
repo, err := newRepo("module")
|
store, err := driver.NewShareStore("sqlite://testdata/test_share_module.sqlite")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
@ -23,10 +25,10 @@ func TestModule(t *testing.T, newRepo NewTestRepoFunc) {
|
|||||||
server := app.NewServer(
|
server := app.NewServer(
|
||||||
module.ContextModuleFactory(),
|
module.ContextModuleFactory(),
|
||||||
module.ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
share.ModuleFactory("test.app.edge", repo),
|
ModuleFactory("test.app.edge", store),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := fs.ReadFile(testData, "testdata/share.js")
|
data, err := os.ReadFile("testdata/share.js")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
@ -1,13 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
1
pkg/module/share/sqlite/testdata/.gitignore
vendored
1
pkg/module/share/sqlite/testdata/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
*.sqlite*
|
|
@ -1,16 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
@ -2,12 +2,12 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"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/sqlite"
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/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 := ioutil.ReadFile("testdata/store.js")
|
data, err := os.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?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&&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>`}
|
||||||
</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 && profile.iss != "anon" ?
|
||||||
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>`
|
||||||
}
|
}
|
||||||
|
35
pkg/storage/driver/blob_store.go
Normal file
35
pkg/storage/driver/blob_store.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
}
|
35
pkg/storage/driver/document_store.go
Normal file
35
pkg/storage/driver/document_store.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
}
|
5
pkg/storage/driver/error.go
Normal file
5
pkg/storage/driver/error.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package driver
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrSchemeNotRegistered = errors.New("scheme was not registered")
|
239
pkg/storage/driver/rpc/client/blob_bucket.go
Normal file
239
pkg/storage/driver/rpc/client/blob_bucket.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
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{}
|
||||||
|
)
|
40
pkg/storage/driver/rpc/client/blob_info.go
Normal file
40
pkg/storage/driver/rpc/client/blob_info.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
|
}
|
101
pkg/storage/driver/rpc/client/blob_store.go
Normal file
101
pkg/storage/driver/rpc/client/blob_store.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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{}
|
87
pkg/storage/driver/rpc/client/blob_store_test.go
Normal file
87
pkg/storage/driver/rpc/client/blob_store_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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
|
||||||
|
}
|
134
pkg/storage/driver/rpc/client/document_store.go
Normal file
134
pkg/storage/driver/rpc/client/document_store.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
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{}
|
67
pkg/storage/driver/rpc/client/document_store_test.go
Normal file
67
pkg/storage/driver/rpc/client/document_store_test.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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
|
||||||
|
}
|
17
pkg/storage/driver/rpc/client/error.go
Normal file
17
pkg/storage/driver/rpc/client/error.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
9
pkg/storage/driver/rpc/client/init.go
Normal file
9
pkg/storage/driver/rpc/client/init.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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
|
150
pkg/storage/driver/rpc/client/share_store.go
Normal file
150
pkg/storage/driver/rpc/client/share_store.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
server "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server/share"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||||
|
"github.com/keegancsmith/rpc"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShareStore struct {
|
||||||
|
serverURL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAttributes implements share.Store.
|
||||||
|
func (s *ShareStore) DeleteAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, names ...string) error {
|
||||||
|
args := server.DeleteAttributesArgs{
|
||||||
|
Origin: origin,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
Names: names,
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := server.DeleteAttributesArgs{}
|
||||||
|
|
||||||
|
if err := s.call(ctx, "Service.DeleteAttributes", args, &reply); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteResource implements share.Store.
|
||||||
|
func (s *ShareStore) DeleteResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) error {
|
||||||
|
args := server.DeleteResourceArgs{
|
||||||
|
Origin: origin,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := server.DeleteResourceReply{}
|
||||||
|
|
||||||
|
if err := s.call(ctx, "Service.DeleteResource", args, &reply); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindResources implements share.Store.
|
||||||
|
func (s *ShareStore) FindResources(ctx context.Context, funcs ...share.FindResourcesOptionFunc) ([]share.Resource, error) {
|
||||||
|
options := share.NewFindResourcesOptions(funcs...)
|
||||||
|
|
||||||
|
args := server.FindResourcesArgs{
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := server.FindResourcesReply{}
|
||||||
|
|
||||||
|
if err := s.call(ctx, "Service.FindResources", args, &reply); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := make([]share.Resource, len(reply.Resources))
|
||||||
|
for idx, res := range reply.Resources {
|
||||||
|
resources[idx] = res
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResource implements share.Store.
|
||||||
|
func (s *ShareStore) GetResource(ctx context.Context, origin app.ID, resourceID share.ResourceID) (share.Resource, error) {
|
||||||
|
args := server.GetResourceArgs{
|
||||||
|
Origin: origin,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := server.GetResourceReply{}
|
||||||
|
|
||||||
|
if err := s.call(ctx, "Service.GetResource", args, &reply); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.Resource, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAttributes implements share.Store.
|
||||||
|
func (s *ShareStore) UpdateAttributes(ctx context.Context, origin app.ID, resourceID share.ResourceID, attributes ...share.Attribute) (share.Resource, error) {
|
||||||
|
serializableAttributes := make([]*server.SerializableAttribute, len(attributes))
|
||||||
|
for attrIdx, attr := range attributes {
|
||||||
|
serializableAttributes[attrIdx] = server.FromAttribute(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := server.UpdateAttributesArgs{
|
||||||
|
Origin: origin,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
Attributes: serializableAttributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := server.UpdateAttributesReply{}
|
||||||
|
|
||||||
|
if err := s.call(ctx, "Service.UpdateAttributes", args, &reply); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.Resource, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShareStore) call(ctx context.Context, serviceMethod string, args any, reply any) error {
|
||||||
|
err := s.withClient(ctx, func(ctx context.Context, client *rpc.Client) error {
|
||||||
|
if err := client.Call(ctx, serviceMethod, args, reply); err != nil {
|
||||||
|
return errors.WithStack(remapShareError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShareStore) withClient(ctx context.Context, fn func(ctx context.Context, client *rpc.Client) error) error {
|
||||||
|
client, err := rpc.DialHTTPPath("tcp", s.serverURL.Host, s.serverURL.Path+"?"+s.serverURL.RawQuery)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
logger.Error(ctx, "could not close rpc client", logger.E(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := fn(ctx, client); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShareStore(url *url.URL) *ShareStore {
|
||||||
|
return &ShareStore{url}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ share.Store = &ShareStore{}
|
67
pkg/storage/driver/rpc/client/share_store_test.go
Normal file
67
pkg/storage/driver/rpc/client/share_store_test.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/share/testsuite"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShareStore(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
testsuite.TestStore(t, func(testName string) (share.Store, error) {
|
||||||
|
httpServer, err := startNewShareStoreServer(testName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverAddr := httpServer.Listener.Addr()
|
||||||
|
serverURL := &url.URL{
|
||||||
|
Host: serverAddr.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewShareStore(serverURL), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSQLiteShareStore(testName string) (*sqlite.ShareStore, error) {
|
||||||
|
filename := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
|
||||||
|
|
||||||
|
file := fmt.Sprintf("./testdata/sharestore_test_%s.sqlite", filename)
|
||||||
|
|
||||||
|
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||||
|
store := sqlite.NewShareStore(dsn)
|
||||||
|
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startNewShareStoreServer(testName string) (*httptest.Server, error) {
|
||||||
|
store, err := getSQLiteShareStore(testName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := server.NewShareStoreServer(store)
|
||||||
|
|
||||||
|
httpServer := httptest.NewServer(server)
|
||||||
|
|
||||||
|
return httpServer, nil
|
||||||
|
}
|
28
pkg/storage/driver/rpc/driver.go
Normal file
28
pkg/storage/driver/rpc/driver.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/client"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
driver.RegisterDocumentStoreFactory("rpc", documentStoreFactory)
|
||||||
|
driver.RegisterBlobStoreFactory("rpc", blobStoreFactory)
|
||||||
|
driver.RegisterShareStoreFactory("rpc", shareStoreFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentStoreFactory(url *url.URL) (storage.DocumentStore, error) {
|
||||||
|
return client.NewDocumentStore(url), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func blobStoreFactory(url *url.URL) (storage.BlobStore, error) {
|
||||||
|
return client.NewBlobStore(url), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shareStoreFactory(url *url.URL) (share.Store, error) {
|
||||||
|
return client.NewShareStore(url), nil
|
||||||
|
}
|
42
pkg/storage/driver/rpc/gob/blob_info.go
Normal file
42
pkg/storage/driver/rpc/gob/blob_info.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package gob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BlobInfo struct {
|
||||||
|
Bucket_ string
|
||||||
|
ContentType_ string
|
||||||
|
BlobID_ storage.BlobID
|
||||||
|
ModTime_ time.Time
|
||||||
|
Size_ int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket implements storage.BlobInfo.
|
||||||
|
func (bi *BlobInfo) Bucket() string {
|
||||||
|
return bi.Bucket_
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentType implements storage.BlobInfo.
|
||||||
|
func (bi *BlobInfo) ContentType() string {
|
||||||
|
return bi.ContentType_
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID implements storage.BlobInfo.
|
||||||
|
func (bi *BlobInfo) ID() storage.BlobID {
|
||||||
|
return bi.BlobID_
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime implements storage.BlobInfo.
|
||||||
|
func (bi *BlobInfo) ModTime() time.Time {
|
||||||
|
return bi.ModTime_
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size implements storage.BlobInfo.
|
||||||
|
func (bi *BlobInfo) Size() int64 {
|
||||||
|
return bi.Size_
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.BlobInfo = &BlobInfo{}
|
18
pkg/storage/driver/rpc/gob/init.go
Normal file
18
pkg/storage/driver/rpc/gob/init.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package gob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(storage.Document{})
|
||||||
|
gob.Register(storage.DocumentID(""))
|
||||||
|
gob.Register(time.Time{})
|
||||||
|
gob.Register(map[string]interface{}{})
|
||||||
|
gob.Register([]interface{}{})
|
||||||
|
gob.Register([]map[string]interface{}{})
|
||||||
|
gob.Register(&BlobInfo{})
|
||||||
|
}
|
31
pkg/storage/driver/rpc/server/blob/close_bucket.go
Normal file
31
pkg/storage/driver/rpc/server/blob/close_bucket.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloseBucketArgs struct {
|
||||||
|
BucketID BucketID
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseBucketReply struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CloseBucket(ctx context.Context, args *CloseBucketArgs, reply *CloseBucketReply) error {
|
||||||
|
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bucket.Close(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.buckets.Delete(args.BucketID)
|
||||||
|
|
||||||
|
*reply = CloseBucketReply{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
31
pkg/storage/driver/rpc/server/blob/close_reader.go
Normal file
31
pkg/storage/driver/rpc/server/blob/close_reader.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloseReaderArgs struct {
|
||||||
|
ReaderID ReaderID
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseReaderReply struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CloseReader(ctx context.Context, args *CloseReaderArgs, reply *CloseReaderReply) error {
|
||||||
|
reader, err := s.getOpenedReader(args.ReaderID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.readers.Delete(args.ReaderID)
|
||||||
|
|
||||||
|
*reply = CloseReaderReply{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
31
pkg/storage/driver/rpc/server/blob/close_writer.go
Normal file
31
pkg/storage/driver/rpc/server/blob/close_writer.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloseWriterArgs struct {
|
||||||
|
WriterID WriterID
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseWriterReply struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CloseWriter(ctx context.Context, args *CloseWriterArgs, reply *CloseWriterReply) error {
|
||||||
|
writer, err := s.getOpenedWriter(args.WriterID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.writers.Delete(args.WriterID)
|
||||||
|
|
||||||
|
*reply = CloseWriterReply{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
22
pkg/storage/driver/rpc/server/blob/delete_bucket.go
Normal file
22
pkg/storage/driver/rpc/server/blob/delete_bucket.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteBucketArgs struct {
|
||||||
|
BucketName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteBucketReply struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteBucket(ctx context.Context, args *DeleteBucketArgs, reply *DeleteBucketReply) error {
|
||||||
|
if err := s.store.DeleteBucket(ctx, args.BucketName); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
42
pkg/storage/driver/rpc/server/blob/get_blob_info.go
Normal file
42
pkg/storage/driver/rpc/server/blob/get_blob_info.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/gob"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetBlobInfoArgs struct {
|
||||||
|
BlobID storage.BlobID
|
||||||
|
BucketID BucketID
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetBlobInfoReply struct {
|
||||||
|
BlobInfo storage.BlobInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetBlobInfo(ctx context.Context, args *GetBlobInfoArgs, reply *GetBlobInfoReply) error {
|
||||||
|
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blobInfo, err := bucket.Get(ctx, args.BlobID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*reply = GetBlobInfoReply{
|
||||||
|
BlobInfo: &gob.BlobInfo{
|
||||||
|
Bucket_: blobInfo.Bucket(),
|
||||||
|
ContentType_: blobInfo.ContentType(),
|
||||||
|
BlobID_: blobInfo.ID(),
|
||||||
|
ModTime_: blobInfo.ModTime(),
|
||||||
|
Size_: blobInfo.Size(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
33
pkg/storage/driver/rpc/server/blob/get_bucket_size.go
Normal file
33
pkg/storage/driver/rpc/server/blob/get_bucket_size.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetBucketSizeArgs struct {
|
||||||
|
BucketID BucketID
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetBucketSizeReply struct {
|
||||||
|
Size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetBucketSize(ctx context.Context, args *GetBucketSizeArgs, reply *GetBucketSizeReply) error {
|
||||||
|
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := bucket.Size(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*reply = GetBucketSizeReply{
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
34
pkg/storage/driver/rpc/server/blob/list_blob_info.go
Normal file
34
pkg/storage/driver/rpc/server/blob/list_blob_info.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListBlobInfoArgs struct {
|
||||||
|
BucketID BucketID
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListBlobInfoReply struct {
|
||||||
|
BlobInfos []storage.BlobInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListBlobInfo(ctx context.Context, args *ListBlobInfoArgs, reply *ListBlobInfoReply) error {
|
||||||
|
bucket, err := s.getOpenedBucket(args.BucketID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blobInfos, err := bucket.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*reply = ListBlobInfoReply{
|
||||||
|
BlobInfos: blobInfos,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
27
pkg/storage/driver/rpc/server/blob/list_buckets.go
Normal file
27
pkg/storage/driver/rpc/server/blob/list_buckets.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListBucketsArgs struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListBucketsReply struct {
|
||||||
|
Buckets []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListBuckets(ctx context.Context, args *ListBucketsArgs, reply *ListBucketsReply) error {
|
||||||
|
buckets, err := s.store.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*reply = ListBucketsReply{
|
||||||
|
Buckets: buckets,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
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