Compare commits

..

2 Commits

Author SHA1 Message Date
Jörn Friedrich Dreyer
b4ef4af69c reduce docker build context
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-12-05 12:17:52 +01:00
Jörn Friedrich Dreyer
0947bf7a94 merge ocdav into frontend
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-12-05 10:33:40 +01:00
97 changed files with 264 additions and 1636 deletions

View File

@@ -1,6 +0,0 @@
{
"repoOwner": "opencloud-eu",
"repoName": "opencloud",
"targetBranchChoices": ["main", "stable-2.0", "stable-4.0"],
"fork": false
}

View File

@@ -36,18 +36,8 @@ ifndef DATE
DATE := $(shell date -u '+%Y%m%d')
endif
LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn -s -w \
-X "$(OC_REPO)/pkg/version.Edition=$(EDITION)" \
-X "$(OC_REPO)/pkg/version.String=$(STRING)" \
-X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" \
-X "$(OC_REPO)/pkg/version.Date=$(DATE)"
DEBUG_LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn \
-X "$(OC_REPO)/pkg/version.Edition=$(EDITION)" \
-X "$(OC_REPO)/pkg/version.String=$(STRING)" \
-X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" \
-X "$(OC_REPO)/pkg/version.Date=$(DATE)"
LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn -s -w -X "$(OC_REPO)/pkg/version.String=$(STRING)" -X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" -X "$(OC_REPO)/pkg/version.Date=$(DATE)"
DEBUG_LDFLAGS += -X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn -X "$(OC_REPO)/pkg/version.String=$(STRING)" -X "$(OC_REPO)/pkg/version.Tag=$(VERSION)" -X "$(OC_REPO)/pkg/version.Date=$(DATE)"
DOCKER_LDFLAGS += -X "$(OC_REPO)/pkg/config/defaults.BaseDataPathType=path" -X "$(OC_REPO)/pkg/config/defaults.BaseDataPathValue=/var/lib/opencloud"
DOCKER_LDFLAGS += -X "$(OC_REPO)/pkg/config/defaults.BaseConfigPathType=path" -X "$(OC_REPO)/pkg/config/defaults.BaseConfigPathValue=/etc/opencloud"

4
.vscode/launch.json vendored
View File

@@ -36,7 +36,7 @@
// demo users
"IDM_CREATE_DEMO_USERS": "true",
// OC_RUN_SERVICES allows to start a subset of services even in the supervised mode
//"OC_RUN_SERVICES": "settings,storage-system,graph,idp,idm,ocs,store,thumbnails,web,webdav,frontend,gateway,users,groups,auth-basic,storage-authmachine,storage-users,storage-shares,storage-publiclink,storage-system,app-provider,sharing,proxy,ocdav",
//"OC_RUN_SERVICES": "settings,storage-system,graph,idp,idm,ocs,store,thumbnails,web,webdav,frontend,gateway,users,groups,auth-basic,storage-authmachine,storage-users,storage-shares,storage-publiclink,storage-system,app-provider,sharing,proxy",
/*
* Keep secrets and passwords in one block to allow easy uncommenting
@@ -129,8 +129,6 @@
"IDP_HTTP_ADDR": "127.0.0.1:10130",
"NATS_DEBUG_ADDR": "127.0.0.1:10234",
"NATS_NATS_PORT": "10233",
"OCDAV_HTTP_ADDR": "127.0.0.1:10350",
"OCDAV_DEBUG_ADDR": "127.0.0.1:10163",
"OCM_DEBUG_ADDR": "127.0.0.1:10281",
"OCM_HTTP_ADDR": "127.0.0.1:10280",
"OCM_GRPC_ADDR": "127.0.0.1:10282",

View File

@@ -1,4 +1,4 @@
# The test runner source for UI tests
WEB_COMMITID=3120ea384c7a9d1f1ea0c328965951fc06d66900
WEB_BRANCH=main
WEB_COMMITID=50e3fff6a518361d59cba864a927470f313b6f91
WEB_BRANCH=stable-4.2

View File

@@ -388,8 +388,6 @@ config = {
"production": {
# NOTE: need to be updated if new production releases are determined
"tags": ["2.0", "4.0"],
# NOTE: need to be set to true if patch releases are made from stable-X-branches
"skip_rolling": "false",
"repo": docker_repo_slug,
"build_type": "production",
},
@@ -481,10 +479,6 @@ def main(ctx):
if ctx.build.event == "cron" and ctx.build.sender == "translation-sync":
return translation_sync(ctx)
is_release_pr = (ctx.build.event == "pull_request" and ctx.build.sender == "openclouders" and "🎉 release" in ctx.build.title.lower())
if is_release_pr:
return [licenseCheck(ctx)]
build_release_helpers = \
readyReleaseGo()
@@ -1614,40 +1608,31 @@ def uploadTracingResult(ctx):
def dockerReleases(ctx):
pipelines = []
docker_releases = []
docker_repos = []
build_type = ""
# only make realeases on tag events
if ctx.build.event == "tag":
tag = ctx.build.ref.replace("refs/tags/v", "").lower()
# iterate over production tags to see if this is a production release
is_production = False
skip_rolling = False
for prod_tag in config["dockerReleases"]["production"]["tags"]:
if tag.startswith(prod_tag):
is_production = True
skip_rolling = config["dockerReleases"]["production"]["skip_rolling"]
break
if is_production:
docker_releases.append("production")
# a new production realease is also a rolling release
# unless skip_rolling is set in the config, i.e. for patch-releases on stable-branch
if not skip_rolling:
docker_releases.append("rolling")
docker_repos.append(config["dockerReleases"]["production"]["repo"])
build_type = config["dockerReleases"]["production"]["build_type"]
else:
docker_releases.append("rolling")
docker_repos.append(config["dockerReleases"]["rolling"]["repo"])
build_type = config["dockerReleases"]["rolling"]["build_type"]
# on non tag events, do daily build
else:
docker_releases.append("daily")
docker_repos.append(config["dockerReleases"]["daily"]["repo"])
build_type = config["dockerReleases"]["daily"]["build_type"]
for releaseConfigName in docker_releases:
repo = config["dockerReleases"][releaseConfigName]["repo"]
build_type = config["dockerReleases"][releaseConfigName]["build_type"]
for repo in docker_repos:
repo_pipelines = []
repo_pipelines.append(dockerRelease(ctx, repo, build_type))
@@ -1667,7 +1652,6 @@ def dockerRelease(ctx, repo, build_type):
build_args = {
"REVISION": "%s" % ctx.build.commit,
"VERSION": "%s" % (ctx.build.ref.replace("refs/tags/", "") if ctx.build.event == "tag" else "daily"),
"EDITION": "stable" if build_type == "production" else "rolling",
}
# if no additional tag is given, the build-plugin adds latest
@@ -1831,7 +1815,6 @@ def binaryRelease(ctx, arch, depends_on = []):
"image": OC_CI_GOLANG,
"environment": {
"VERSION": (ctx.build.ref.replace("refs/tags/", "") if ctx.build.event == "tag" else "daily"),
"EDITION": "rolling",
"HTTP_PROXY": {
"from_secret": "ci_http_proxy",
},
@@ -2137,7 +2120,6 @@ def opencloudServer(storage = "decomposed", accounts_hash_difficulty = 4, depend
"IDP_DEBUG_ADDR": "0.0.0.0:9134",
"INVITATIONS_DEBUG_ADDR": "0.0.0.0:9269",
"NATS_DEBUG_ADDR": "0.0.0.0:9234",
"OCDAV_DEBUG_ADDR": "0.0.0.0:9163",
"OCM_DEBUG_ADDR": "0.0.0.0:9281",
"OCS_DEBUG_ADDR": "0.0.0.0:9114",
"POSTPROCESSING_DEBUG_ADDR": "0.0.0.0:9255",
@@ -2357,12 +2339,11 @@ def translation_sync(ctx):
"image": OC_CI_GOLANG,
"commands": [
"make l10n-read",
"mkdir tx && cd tx",
"curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash",
"export PATH=$PATH:$(pwd) && cd ..",
". ~/.profile",
"make l10n-push",
"make l10n-pull",
"rm -rf tx",
"rm tx",
"make l10n-clean",
],
"environment": {

View File

@@ -1,41 +1,5 @@
# Changelog
## [4.1.0](https://github.com/opencloud-eu/opencloud/releases/tag/v4.1.0) - 2025-12-15
### ❤️ Thanks to all contributors! ❤️
@JammingBen, @ScharfViktor, @Svanvith, @butonic, @flimmy, @fschade, @individual-it, @kulmann, @micbar, @prashant-gurung899, @saw-jan
### 📚 Documentation
- fix typo [[#2024](https://github.com/opencloud-eu/opencloud/pull/2024)]
- [docs] update policies link [[#1996](https://github.com/opencloud-eu/opencloud/pull/1996)]
- fix the link in quickstart script for itself [[#1956](https://github.com/opencloud-eu/opencloud/pull/1956)]
### ✅ Tests
- [full-ci][tests-only] test: fix some test flakiness [[#2003](https://github.com/opencloud-eu/opencloud/pull/2003)]
- [tests-only] Skip test related pipelines for ready-release-go PRs [[#2011](https://github.com/opencloud-eu/opencloud/pull/2011)]
- [full-ci][tests-only] test: add test to check mismatch offset during TUS upload [[#1993](https://github.com/opencloud-eu/opencloud/pull/1993)]
- [full-ci][tests-only] test: proper resource existence check [[#1990](https://github.com/opencloud-eu/opencloud/pull/1990)]
- check propfing after renaming data in file system [[#1809](https://github.com/opencloud-eu/opencloud/pull/1809)]
- fix-get-attribute-test [[#1974](https://github.com/opencloud-eu/opencloud/pull/1974)]
### 📈 Enhancement
- Show edition in opencloud version command [[#2019](https://github.com/opencloud-eu/opencloud/pull/2019)]
### 🐛 Bug Fixes
- fix: enforce trailing slash for server url [[#1995](https://github.com/opencloud-eu/opencloud/pull/1995)]
- fix: enhance resource creation with detailed process information [[#1978](https://github.com/opencloud-eu/opencloud/pull/1978)]
### 📦️ Dependencies
- chore: bump web to v4.3.0 [[#2030](https://github.com/opencloud-eu/opencloud/pull/2030)]
- reva-bump-2.41.0 [[#2032](https://github.com/opencloud-eu/opencloud/pull/2032)]
- build(deps): bump github.com/testcontainers/testcontainers-go from 0.39.0 to 0.40.0 [[#1931](https://github.com/opencloud-eu/opencloud/pull/1931)]
## [4.0.0](https://github.com/opencloud-eu/opencloud/releases/tag/v4.0.0) - 2025-12-01
### ❤️ Thanks to all contributors! ❤️

View File

@@ -44,7 +44,6 @@ OC_MODULES = \
services/invitations \
services/nats \
services/notifications \
services/ocdav \
services/ocm \
services/ocs \
services/policies \

2
go.mod
View File

@@ -64,7 +64,7 @@ require (
github.com/open-policy-agent/opa v1.10.1
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
github.com/opencloud-eu/reva/v2 v2.41.0
github.com/opencloud-eu/reva/v2 v2.40.1
github.com/opensearch-project/opensearch-go/v4 v4.5.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1

4
go.sum
View File

@@ -963,8 +963,8 @@ github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIft
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76 h1:vD/EdfDUrv4omSFjrinT8Mvf+8D7f9g4vgQ2oiDrVUI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
github.com/opencloud-eu/reva/v2 v2.41.0 h1:oie8+sxcA+drREXRTqm0LmfUdy/mmaa6pA6wkdF6tF4=
github.com/opencloud-eu/reva/v2 v2.41.0/go.mod h1:DGH08n2mvtsQLkt8o15FV6m51FwSJJGhjR8Ty+iIJww=
github.com/opencloud-eu/reva/v2 v2.40.1 h1:QwMkbGMhwDSwfk2WxbnTpIig2BugPBaVFjWcy2DSU3U=
github.com/opencloud-eu/reva/v2 v2.40.1/go.mod h1:DGH08n2mvtsQLkt8o15FV6m51FwSJJGhjR8Ty+iIJww=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=

View File

@@ -17,7 +17,7 @@ include ../.make/docs.mk
.PHONY: dev-docker
dev-docker:
docker build -f docker/Dockerfile.multiarch -t opencloudeu/opencloud:dev ../..
docker build -f docker/Dockerfile.multiarch -t opencloudeu/opencloud:dev ..
.PHONY: dev-docker-multiarch
dev-docker-multiarch:

View File

@@ -3,7 +3,6 @@ ARG TARGETOS
ARG TARGETARCH
ARG VERSION
ARG STRING
ARG EDITION
RUN apk add bash make git curl gcc musl-dev libc-dev binutils-gold inotify-tools vips-dev
@@ -12,7 +11,7 @@ RUN --mount=type=bind,target=/build,rw \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache \
GOOS="${TARGETOS:-linux}" GOARCH="${TARGETARCH:-amd64}" ; \
make -C opencloud/opencloud release-linux-docker-${TARGETARCH} ENABLE_VIPS=true DIST=/dist
make -C opencloud release-linux-docker-${TARGETARCH} ENABLE_VIPS=true DIST=/dist
FROM alpine:3.22
ARG VERSION

View File

@@ -30,7 +30,6 @@ import (
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command"
nats "github.com/opencloud-eu/opencloud/services/nats/pkg/command"
notifications "github.com/opencloud-eu/opencloud/services/notifications/pkg/command"
ocdav "github.com/opencloud-eu/opencloud/services/ocdav/pkg/command"
ocm "github.com/opencloud-eu/opencloud/services/ocm/pkg/command"
ocs "github.com/opencloud-eu/opencloud/services/ocs/pkg/command"
policies "github.com/opencloud-eu/opencloud/services/policies/pkg/command"
@@ -163,11 +162,6 @@ var svccmds = []register.Command{
cfg.Notifications.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cli.Command {
return ServiceCommand(cfg, cfg.OCDav.Service.Name, ocdav.GetCommands(cfg.OCDav), func(c *config.Config) {
cfg.OCDav.Commons = cfg.Commons
})
},
func(cfg *config.Config) *cli.Command {
return ServiceCommand(cfg, cfg.OCM.Service.Name, ocm.GetCommands(cfg.OCM), func(c *config.Config) {
cfg.OCM.Commons = cfg.Commons

View File

@@ -33,7 +33,6 @@ func VersionCommand(cfg *config.Config) *cli.Command {
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Edition: %s\n", version.Edition)
fmt.Printf("Compiled: %s\n", version.Compiled())
if c.Bool(_skipServiceListingFlagName) {

View File

@@ -281,6 +281,7 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
cfg.Collaboration.App.Insecure = true
cfg.Frontend.AppHandler = _insecureService
cfg.Frontend.Archiver = _insecureService
cfg.Frontend.OCDav = _insecureService
cfg.Graph.Spaces = _insecureService
cfg.Graph.Events = _insecureEvents
cfg.Notifications.Notifications.Events = _insecureEvents
@@ -289,7 +290,6 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
cfg.Sharing.Events = _insecureEvents
cfg.StorageUsers.Events = _insecureEvents
cfg.Nats.Nats.TLSSkipVerifyClientCert = true
cfg.Ocdav = _insecureService
cfg.Proxy = ProxyService{
InsecureBackends: true,
OIDC: InsecureProxyOIDC{

View File

@@ -32,7 +32,6 @@ type OpenCloudConfig struct {
AuthBearer AuthbearerService `yaml:"auth_bearer"`
Users UsersAndGroupsService `yaml:"users"`
Groups UsersAndGroupsService `yaml:"groups"`
Ocdav InsecureService `yaml:"ocdav"`
Ocm OcmService `yaml:"ocm"`
Thumbnails ThumbnailService `yaml:"thumbnails"`
Search Search `yaml:"search"`
@@ -105,6 +104,7 @@ type FrontendService struct {
AppHandler InsecureService `yaml:"app_handler"`
Archiver InsecureService
ServiceAccount ServiceAccount `yaml:"service_account"`
OCDav InsecureService
}
// Gateway is the configuration for the gateway

View File

@@ -40,7 +40,6 @@ import (
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command"
nats "github.com/opencloud-eu/opencloud/services/nats/pkg/command"
notifications "github.com/opencloud-eu/opencloud/services/notifications/pkg/command"
ocdav "github.com/opencloud-eu/opencloud/services/ocdav/pkg/command"
ocm "github.com/opencloud-eu/opencloud/services/ocm/pkg/command"
ocs "github.com/opencloud-eu/opencloud/services/ocs/pkg/command"
policies "github.com/opencloud-eu/opencloud/services/policies/pkg/command"
@@ -204,11 +203,6 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) {
cfg.IDM.Commons = cfg.Commons
return idm.Execute(cfg.IDM)
})
reg(3, opts.Config.OCDav.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.OCDav.Context = ctx
cfg.OCDav.Commons = cfg.Commons
return ocdav.Execute(cfg.OCDav)
})
reg(3, opts.Config.OCS.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
cfg.OCS.Context = ctx
cfg.OCS.Commons = cfg.Commons

View File

@@ -24,7 +24,6 @@ import (
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config"
nats "github.com/opencloud-eu/opencloud/services/nats/pkg/config"
notifications "github.com/opencloud-eu/opencloud/services/notifications/pkg/config"
ocdav "github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
ocm "github.com/opencloud-eu/opencloud/services/ocm/pkg/config"
ocs "github.com/opencloud-eu/opencloud/services/ocs/pkg/config"
policies "github.com/opencloud-eu/opencloud/services/policies/pkg/config"
@@ -105,7 +104,6 @@ type Config struct {
Invitations *invitations.Config `yaml:"invitations"`
Nats *nats.Config `yaml:"nats"`
Notifications *notifications.Config `yaml:"notifications"`
OCDav *ocdav.Config `yaml:"ocdav"`
OCM *ocm.Config `yaml:"ocm"`
OCS *ocs.Config `yaml:"ocs"`
Postprocessing *postprocessing.Config `yaml:"postprocessing"`

View File

@@ -24,7 +24,6 @@ import (
invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config/defaults"
nats "github.com/opencloud-eu/opencloud/services/nats/pkg/config/defaults"
notifications "github.com/opencloud-eu/opencloud/services/notifications/pkg/config/defaults"
ocdav "github.com/opencloud-eu/opencloud/services/ocdav/pkg/config/defaults"
ocm "github.com/opencloud-eu/opencloud/services/ocm/pkg/config/defaults"
ocs "github.com/opencloud-eu/opencloud/services/ocs/pkg/config/defaults"
policies "github.com/opencloud-eu/opencloud/services/policies/pkg/config/defaults"
@@ -80,7 +79,6 @@ func DefaultConfig() *Config {
Invitations: invitations.DefaultConfig(),
Nats: nats.DefaultConfig(),
Notifications: notifications.DefaultConfig(),
OCDav: ocdav.DefaultConfig(),
OCM: ocm.DefaultConfig(),
OCS: ocs.DefaultConfig(),
Postprocessing: postprocessing.DefaultConfig(),

View File

@@ -1,9 +1,10 @@
package config
import (
"gotest.tools/v3/assert"
"testing"
"testing/fstest"
"gotest.tools/v3/assert"
)
type TestConfig struct {
@@ -98,6 +99,8 @@ frontend:
service_account:
service_account_id: c05389b2-d94c-4d01-a9b5-a2f97952cc14
service_account_secret: GW5.x1vDM&+NPRi++eV@.P7Tms4vj!=s
ocdav:
insecure: true
auth_basic:
auth_providers:
ldap:
@@ -114,8 +117,6 @@ groups:
drivers:
ldap:
bind_password: c68JL=V$c@0GHs!%eSb8r&Ps3rgzKnXJ
ocdav:
insecure: true
ocm:
service_account:
service_account_id: c05389b2-d94c-4d01-a9b5-a2f97952cc14

View File

@@ -105,19 +105,9 @@ func createResource(ctx context.Context, serviceName string) (*resource.Resource
return resource.New(ctx,
// Reads OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME
resource.WithFromEnv(),
// Host Information
// Host and process information
resource.WithHost(),
// Process Information
// Resource WithProcessOwner is deliberately omitted because
// inside containers where process might run as an arbitrary
// uid without a username associated this would fail.
resource.WithProcessPID(),
resource.WithProcessCommandArgs(),
resource.WithProcessExecutableName(),
resource.WithProcessExecutablePath(),
resource.WithProcessRuntimeDescription(),
resource.WithProcessRuntimeName(),
resource.WithProcessRuntimeVersion(),
resource.WithProcess(),
// Service attributes
resource.WithAttributes(
semconv.ServiceName(serviceName),

View File

@@ -1,4 +0,0 @@
package version
// InitEdition exports the private edition initialization func for testing
var InitEdition = initEdition

View File

@@ -1,27 +1,9 @@
package version
import (
"fmt"
"slices"
"strings"
"time"
"github.com/Masterminds/semver"
"github.com/opencloud-eu/reva/v2/pkg/logger"
)
const (
// Dev is used as a placeholder.
Dev = "dev"
// EditionDev indicates the development build channel was used to build the binary.
EditionDev = Dev
// EditionRolling indicates the rolling release build channel was used to build the binary.
EditionRolling = "rolling"
// EditionStable indicates the stable release build channel was used to build the binary.
EditionStable = "stable"
// EditionLTS indicates the lts release build channel was used to build the binary.
EditionLTS = "lts"
)
var (
@@ -39,56 +21,17 @@ var (
// Date indicates the build date.
// This has been removed, it looks like you can only replace static strings with recent go versions
//Date = time.Now().Format("20060102")
Date = Dev
Date = "dev"
// Legacy defines the old long 4 number OpenCloud version needed for some clients
Legacy = "0.1.0.0"
// LegacyString defines the old OpenCloud version needed for some clients
LegacyString = "0.1.0"
// Edition describes the build channel (stable, rolling, nightly, daily, dev)
Edition = Dev // default for self-compiled builds
)
func init() { //nolint:gochecknoinits
if err := initEdition(); err != nil {
logger.New().Error().Err(err).Msg("falling back to dev")
}
}
func initEdition() error {
regularEditions := []string{EditionDev, EditionRolling, EditionStable}
versionedEditions := []string{EditionLTS}
if !slices.ContainsFunc(slices.Concat(regularEditions, versionedEditions), func(s string) bool {
isRegularEdition := slices.Contains(regularEditions, Edition)
if isRegularEdition && s == Edition {
return true
}
// handle editions with a version
editionParts := strings.Split(Edition, "-")
if len(editionParts) != 2 { // a versioned edition channel must consist of exactly 2 parts.
return false
}
isVersionedEdition := slices.Contains(versionedEditions, editionParts[0])
if !isVersionedEdition { // not all channels can contain version information
return false
}
_, err := semver.NewVersion(editionParts[1])
return err == nil
}) {
Edition = Dev
return fmt.Errorf(`unknown edition channel "%s"`, Edition)
}
return nil
}
// Compiled returns the compile time of this service.
func Compiled() time.Time {
if Date == Dev {
if Date == "dev" {
return time.Now()
}
t, _ := time.Parse("20060102", Date)

View File

@@ -1,65 +0,0 @@
package version_test
import (
"fmt"
"testing"
"github.com/opencloud-eu/opencloud/pkg/version"
)
func TestChannel(t *testing.T) {
tests := map[string]struct {
got string
valid bool
}{
"no channel, defaults to dev": {
got: "",
valid: false,
},
"dev channel": {
got: version.EditionDev,
valid: true,
},
"rolling channel": {
got: version.EditionRolling,
valid: true,
},
"stable channel": {
got: version.EditionStable,
valid: true,
},
"lts channel without version": {
got: version.EditionLTS,
valid: false,
},
"lts-1.0.0 channel": {
got: fmt.Sprintf("%s-1", version.EditionLTS),
valid: true,
},
"lts-one invalid version": {
got: fmt.Sprintf("%s-one", version.EditionLTS),
valid: false,
},
"known channel with version": {
got: fmt.Sprintf("%s-1", version.EditionStable),
valid: false,
},
"unknown channel": {
got: "foo",
valid: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
version.Edition = test.got
switch err := version.InitEdition(); {
case err != nil && !test.valid && version.Edition != version.Dev: // if a given edition is unknown, the value is always dev
fallthrough
case test.valid != (err == nil):
t.Fatalf("invalid edition: %s", version.Edition)
}
})
}
}

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-17 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -33,7 +33,7 @@ type Config struct {
EnableFederatedSharingIncoming bool `yaml:"enable_federated_sharing_incoming" env:"OC_ENABLE_OCM;FRONTEND_ENABLE_FEDERATED_SHARING_INCOMING" desc:"Changing this value is NOT supported. Enables support for incoming federated sharing for clients. The backend behaviour is not changed." introductionVersion:"1.0.0"`
EnableFederatedSharingOutgoing bool `yaml:"enable_federated_sharing_outgoing" env:"OC_ENABLE_OCM;FRONTEND_ENABLE_FEDERATED_SHARING_OUTGOING" desc:"Changing this value is NOT supported. Enables support for outgoing federated sharing for clients. The backend behaviour is not changed." introductionVersion:"1.0.0"`
SearchMinLength int `yaml:"search_min_length" env:"FRONTEND_SEARCH_MIN_LENGTH" desc:"Minimum number of characters to enter before a client should start a search for Share receivers. This setting can be used to customize the user experience if e.g too many results are displayed." introductionVersion:"1.0.0"`
Edition string `desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
Edition string `yaml:"edition" env:"OC_EDITION;FRONTEND_EDITION" desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
DisableSSE bool `yaml:"disable_sse" env:"OC_DISABLE_SSE;FRONTEND_DISABLE_SSE" desc:"When set to true, clients are informed that the Server-Sent Events endpoint is not accessible." introductionVersion:"1.0.0"`
DisableRadicale bool `yaml:"disable_radicale" env:"FRONTEND_DISABLE_RADICALE" desc:"When set to true, clients are informed that the Radicale (CalDAV/CardDAV) is not accessible." introductionVersion:"4.0.0"`
DefaultLinkPermissions int `yaml:"default_link_permissions" env:"FRONTEND_DEFAULT_LINK_PERMISSIONS" desc:"Defines the default permissions a link is being created with. Possible values are 0 (= internal link, for instance members only) and 1 (= public link with viewer permissions). Defaults to 1." introductionVersion:"1.0.0"`
@@ -44,6 +44,7 @@ type Config struct {
Archiver Archiver `yaml:"archiver"`
DataGateway DataGateway `yaml:"data_gateway"`
OCS OCS `yaml:"ocs"`
OCDav OCDav `yaml:"ocdav"`
Checksums Checksums `yaml:"checksums"`
ReadOnlyUserAttributes []string `yaml:"read_only_user_attributes" env:"FRONTEND_READONLY_USER_ATTRIBUTES" desc:"A list of user attributes to indicate as read-only. Supported values: 'user.onPremisesSamAccountName' (username), 'user.displayName', 'user.mail', 'user.passwordProfile' (password), 'user.appRoleAssignments' (role), 'user.memberOf' (groups), 'user.accountEnabled' (login allowed), 'drive.quota' (quota). See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
LDAPServerWriteEnabled bool `yaml:"ldap_server_write_enabled" env:"OC_LDAP_SERVER_WRITE_ENABLED;FRONTEND_LDAP_SERVER_WRITE_ENABLED" desc:"Allow creating, modifying and deleting LDAP users via the GRAPH API. This can only be set to 'true' when keeping default settings for the LDAP user and group attribute types (the 'OC_LDAP_USER_SCHEMA_* and 'OC_LDAP_GROUP_SCHEMA_* variables)." introductionVersion:"1.0.0"`
@@ -152,6 +153,41 @@ type OCS struct {
ShowUserEmailInResults bool `yaml:"show_email_in_results" env:"OC_SHOW_USER_EMAIL_IN_RESULTS" desc:"Include user email addresses in responses. If absent or set to false emails will be omitted from results. Please note that admin users can always see all email addresses." introductionVersion:"1.0.0"`
}
type OCDav struct {
Prefix string `yaml:"prefix" env:"OCDAV_HTTP_PREFIX" desc:"A URL path prefix for the handler." introductionVersion:"1.0.0"`
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"OCDAV_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"1.0.0"`
WebdavNamespace string `yaml:"webdav_namespace" env:"OCDAV_WEBDAV_NAMESPACE" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties." introductionVersion:"1.0.0"`
FilesNamespace string `yaml:"files_namespace" env:"OCDAV_FILES_NAMESPACE" desc:"Jail requests to /dav/files/{username} into this CS3 namespace. Supports template layouting with CS3 User properties." introductionVersion:"1.0.0"`
SharesNamespace string `yaml:"shares_namespace" env:"OCDAV_SHARES_NAMESPACE" desc:"The human readable path for the share jail. Relative to a users personal space root. Upcased intentionally." introductionVersion:"1.0.0"`
OCMNamespace string `yaml:"ocm_namespace" env:"OCDAV_OCM_NAMESPACE" desc:"The human readable path prefix for the ocm shares." introductionVersion:"1.0.0"`
// PublicURL used to redirect /s/{token} URLs to
PublicURL string `yaml:"public_url" env:"OC_URL;OCDAV_PUBLIC_URL" desc:"URL where OpenCloud is reachable for users." introductionVersion:"1.0.0"`
// Insecure certificates allowed when making requests to the gateway
Insecure bool `yaml:"insecure" env:"OC_INSECURE;OCDAV_INSECURE" desc:"Allow insecure connections to the GATEWAY service." introductionVersion:"1.0.0"`
EnableHTTPTPC bool `yaml:"enable_http_tpc" env:"OCDAV_ENABLE_HTTP_TPC" desc:"Enable HTTP / WebDAV Third-Party-Copy support." introductionVersion:"%%NEXT%%"`
// Timeout in seconds when making requests to the gateway
Timeout int64 `yaml:"gateway_request_timeout" env:"OCDAV_GATEWAY_REQUEST_TIMEOUT" desc:"Request timeout in seconds for requests from the oCDAV service to the GATEWAY service." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;OCDAV_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"1.0.0"`
Status Status `yaml:"-"`
AllowPropfindDepthInfinity bool `yaml:"allow_propfind_depth_infinity" env:"OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY" desc:"Allow the use of depth infinity in PROPFINDS. When enabled, a propfind will traverse through all subfolders. If many subfolders are expected, depth infinity can cause heavy server load and/or delayed response times." introductionVersion:"1.0.0"`
}
// Status holds the configurable values for the status.php
type Status struct {
Version string
VersionString string
Product string
ProductName string
ProductVersion string
Edition string `yaml:"edition" env:"OC_EDITION;OCDAV_EDITION" desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
}
type CacheWarmupDrivers struct {
CBOX CBOXDriver `yaml:"cbox,omitempty"`
}

View File

@@ -88,7 +88,7 @@ func DefaultConfig() *config.Config {
DefaultUploadProtocol: "tus",
DefaultLinkPermissions: 1,
SearchMinLength: 3,
Edition: version.Edition,
Edition: "",
CheckForUpdates: true,
Checksums: config.Checksums{
SupportedTypes: []string{"sha1", "md5", "adler32"},
@@ -120,6 +120,28 @@ func DefaultConfig() *config.Config {
PublicShareMustHavePassword: true,
IncludeOCMSharees: false,
},
OCDav: config.OCDav{
Prefix: "",
SkipUserGroupsInToken: false,
WebdavNamespace: "/users/{{.Id.OpaqueId}}",
FilesNamespace: "/users/{{.Id.OpaqueId}}",
SharesNamespace: "/Shares",
OCMNamespace: "/public",
PublicURL: "https://localhost:9200",
Insecure: false,
EnableHTTPTPC: false,
Timeout: 84300,
Status: config.Status{
Version: version.Legacy,
VersionString: version.LegacyString,
ProductVersion: version.GetString(),
Product: "OpenCloud",
ProductName: "OpenCloud",
Edition: "",
},
AllowPropfindDepthInfinity: false,
},
Middleware: config.Middleware{
Auth: config.Auth{
CredentialsByUserAgent: map[string]string{},

View File

@@ -346,7 +346,7 @@ func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string
},
"version": map[string]interface{}{
"product": "OpenCloud",
"edition": version.Edition,
"edition": "",
"major": version.ParsedLegacy().Major(),
"minor": version.ParsedLegacy().Minor(),
"micro": version.ParsedLegacy().Patch(),
@@ -357,6 +357,34 @@ func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string
"include_ocm_sharees": cfg.OCS.IncludeOCMSharees,
"show_email_in_results": cfg.OCS.ShowUserEmailInResults,
},
"ocdav": map[string]interface{}{
"prefix": cfg.OCDav.Prefix,
"files_namespace": cfg.OCDav.FilesNamespace,
"webdav_namespace": cfg.OCDav.WebdavNamespace,
"shares_namespace": cfg.OCDav.SharesNamespace,
"ocm_namespace": cfg.OCDav.OCMNamespace,
"gatewaysvc": cfg.Reva.Address,
"timeout": cfg.OCDav.Timeout,
"insecure": cfg.OCDav.Insecure,
"enable_http_tpc": cfg.OCDav.EnableHTTPTPC,
"public_url": cfg.OCDav.PublicURL,
// still not supported
//"favorite_storage_driver": unused,
//"favorite_storage_drivers": unused,
"version": cfg.OCDav.Status.Version,
"version_string": cfg.OCDav.Status.VersionString,
"edition": cfg.OCDav.Status.Edition,
"product": cfg.OCDav.Status.Product,
"product_name": cfg.OCDav.Status.ProductName,
"product_version": cfg.OCDav.Status.ProductVersion,
"allow_depth_infinity": cfg.OCDav.AllowPropfindDepthInfinity,
"validation": map[string]interface{}{
// "invalid_chars": aka ItemNameInvalidChars option ... unused
// "max_length": aka ItemNameMaxLength option ... unused
},
"url_signing_shared_secret": cfg.Commons.URLSigningSecret,
"machine_auth_apikey": cfg.MachineAuthAPIKey,
},
},
},
}, nil

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-15 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-15 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/opencloud-eu/teams/204053/pl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-17 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-21 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/opencloud-eu/teams/204053/ru/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-23 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: miguel tapias, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/opencloud-eu/teams/204053/es/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-14 00:02+0000\n"
"POT-Creation-Date: 2025-11-24 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-15 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-18 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -12,7 +12,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-16 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/opencloud-eu/teams/204053/ru/)\n"

View File

@@ -1,11 +0,0 @@
SHELL := bash
NAME := ocdav
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
include ../../.make/default.mk
include ../../.make/go.mk
include ../../.make/release.mk
include ../../.make/docs.mk

View File

@@ -1,3 +0,0 @@
# ocDAV
The ocdav service provides the WebDAV API which is required by OpenCloud clients. Previews (thumbnails) are provided by the [WebDAV service](../webdav).

View File

@@ -1,19 +0,0 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/command"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config/defaults"
)
func main() {
cfg := defaults.DefaultConfig()
cfg.Context, _ = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
if err := command.Execute(cfg); err != nil {
os.Exit(1)
}
}

View File

@@ -1,54 +0,0 @@
package command
import (
"fmt"
"net/http"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config/parser"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/logging"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "check health status",
Category: "info",
Before: func(c *cli.Context) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}

View File

@@ -1,34 +0,0 @@
package command
import (
"os"
"github.com/opencloud-eu/opencloud/pkg/clihelper"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the OpenCloud ocdav command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "ocdav",
Usage: "Provide a WebDav API for OpenCloud",
Commands: GetCommands(cfg),
})
return app.RunContext(cfg.Context, os.Args)
}

View File

@@ -1,134 +0,0 @@
package command
import (
"context"
"fmt"
"os/signal"
"github.com/opencloud-eu/opencloud/pkg/broker"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/runner"
ohttp "github.com/opencloud-eu/opencloud/pkg/service/http"
"github.com/opencloud-eu/opencloud/pkg/tracing"
"github.com/opencloud-eu/opencloud/pkg/version"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config/parser"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/logging"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/server/debug"
"github.com/opencloud-eu/reva/v2/pkg/micro/ocdav"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/urfave/cli/v2"
)
// Server is the entry point for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
traceProvider, err := tracing.GetTraceProvider(c.Context, cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
var cancel context.CancelFunc
if cfg.Context == nil {
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
defer cancel()
}
ctx := cfg.Context
gr := runner.NewGroup()
// init reva shared config explicitly as the go-micro based ocdav does not use
// the reva runtime. But we need e.g. the shared client settings to be initialized
sc := map[string]interface{}{
"jwt_secret": cfg.TokenManager.JWTSecret,
"gatewaysvc": cfg.Reva.Address,
"skip_user_groups_in_token": cfg.SkipUserGroupsInToken,
"grpc_client_options": cfg.Reva.GetGRPCClientConfig(),
}
if err := sharedconf.Decode(sc); err != nil {
logger.Error().Err(err).Msg("error decoding shared config for ocdav")
}
opts := []ocdav.Option{
ocdav.Name(cfg.HTTP.Namespace + "." + cfg.Service.Name),
ocdav.Version(version.GetString()),
ocdav.Context(ctx),
ocdav.Logger(logger.Logger),
ocdav.Address(cfg.HTTP.Addr),
ocdav.AllowCredentials(cfg.HTTP.CORS.AllowCredentials),
ocdav.AllowedMethods(cfg.HTTP.CORS.AllowedMethods),
ocdav.AllowedHeaders(cfg.HTTP.CORS.AllowedHeaders),
ocdav.AllowedOrigins(cfg.HTTP.CORS.AllowedOrigins),
ocdav.FilesNamespace(cfg.FilesNamespace),
ocdav.WebdavNamespace(cfg.WebdavNamespace),
ocdav.OCMNamespace(cfg.OCMNamespace),
ocdav.AllowDepthInfinity(cfg.AllowPropfindDepthInfinity),
ocdav.SharesNamespace(cfg.SharesNamespace),
ocdav.Timeout(cfg.Timeout),
ocdav.Insecure(cfg.Insecure),
ocdav.PublicURL(cfg.PublicURL),
ocdav.Prefix(cfg.HTTP.Prefix),
ocdav.GatewaySvc(cfg.Reva.Address),
ocdav.JWTSecret(cfg.TokenManager.JWTSecret),
ocdav.ProductName(cfg.Status.ProductName),
ocdav.ProductVersion(cfg.Status.ProductVersion),
ocdav.Product(cfg.Status.Product),
ocdav.Version(cfg.Status.Version),
ocdav.VersionString(cfg.Status.VersionString),
ocdav.Edition(cfg.Status.Edition),
ocdav.MachineAuthAPIKey(cfg.MachineAuthAPIKey),
ocdav.Broker(broker.NoOp{}),
// ocdav.FavoriteManager() // FIXME needs a proper persistence implementation https://github.com/owncloud/ocis/issues/1228
// ocdav.LockSystem(), // will default to the CS3 lock system
// ocdav.TLSConfig() // tls config for the http server
ocdav.MetricsEnabled(true),
ocdav.MetricsNamespace("ocis"),
ocdav.WithTraceProvider(traceProvider),
ocdav.RegisterTTL(registry.GetRegisterTTL()),
ocdav.RegisterInterval(registry.GetRegisterInterval()),
ocdav.URLSigningSharedSecret(cfg.Commons.URLSigningSecret),
}
s, err := ocdav.Service(opts...)
if err != nil {
return err
}
// creating a runner for a go-micro service is a bit complex, so we'll
// wrap the go-micro service with an ocis service the same way as
// ocis-pkg/service/http is doing in order to reuse the factory.
gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", ohttp.Service{Service: s}))
debugServer, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("server", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
grResults := gr.Run(ctx)
// return the first non-nil error found in the results
for _, grResult := range grResults {
if grResult.RunnerError != nil {
return grResult.RunnerError
}
}
return nil
},
}
}

View File

@@ -1,50 +0,0 @@
package command
import (
"fmt"
"os"
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/version"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running service instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}

View File

@@ -1,84 +0,0 @@
package config
import (
"context"
"github.com/opencloud-eu/opencloud/pkg/shared"
)
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
Log *Log `yaml:"log"`
Debug Debug `yaml:"debug"`
HTTP HTTPConfig `yaml:"http"`
TokenManager *TokenManager `yaml:"token_manager"`
Reva *shared.Reva `yaml:"reva"`
SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"OCDAV_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"1.0.0"`
WebdavNamespace string `yaml:"webdav_namespace" env:"OCDAV_WEBDAV_NAMESPACE" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties." introductionVersion:"1.0.0"`
FilesNamespace string `yaml:"files_namespace" env:"OCDAV_FILES_NAMESPACE" desc:"Jail requests to /dav/files/{username} into this CS3 namespace. Supports template layouting with CS3 User properties." introductionVersion:"1.0.0"`
SharesNamespace string `yaml:"shares_namespace" env:"OCDAV_SHARES_NAMESPACE" desc:"The human readable path for the share jail. Relative to a users personal space root. Upcased intentionally." introductionVersion:"1.0.0"`
OCMNamespace string `yaml:"ocm_namespace" env:"OCDAV_OCM_NAMESPACE" desc:"The human readable path prefix for the ocm shares." introductionVersion:"1.0.0"`
// PublicURL used to redirect /s/{token} URLs to
PublicURL string `yaml:"public_url" env:"OC_URL;OCDAV_PUBLIC_URL" desc:"URL where OpenCloud is reachable for users." introductionVersion:"1.0.0"`
// Insecure certificates allowed when making requests to the gateway
Insecure bool `yaml:"insecure" env:"OC_INSECURE;OCDAV_INSECURE" desc:"Allow insecure connections to the GATEWAY service." introductionVersion:"1.0.0"`
// Timeout in seconds when making requests to the gateway
Timeout int64 `yaml:"gateway_request_timeout" env:"OCDAV_GATEWAY_REQUEST_TIMEOUT" desc:"Request timeout in seconds for requests from the oCDAV service to the GATEWAY service." introductionVersion:"1.0.0"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;OCDAV_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"1.0.0"`
Context context.Context `yaml:"-"`
Status Status `yaml:"-"`
AllowPropfindDepthInfinity bool `yaml:"allow_propfind_depth_infinity" env:"OCDAV_ALLOW_PROPFIND_DEPTH_INFINITY" desc:"Allow the use of depth infinity in PROPFINDS. When enabled, a propfind will traverse through all subfolders. If many subfolders are expected, depth infinity can cause heavy server load and/or delayed response times." introductionVersion:"1.0.0"`
}
type Log struct {
Level string `yaml:"level" env:"OC_LOG_LEVEL;OCDAV_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
Pretty bool `yaml:"pretty" env:"OC_LOG_PRETTY;OCDAV_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"1.0.0"`
Color bool `yaml:"color" env:"OC_LOG_COLOR;OCDAV_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"1.0.0"`
File string `yaml:"file" env:"OC_LOG_FILE;OCDAV_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"1.0.0"`
}
type Service struct {
Name string `yaml:"-"`
}
type Debug struct {
Addr string `yaml:"addr" env:"OCDAV_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
Token string `yaml:"token" env:"OCDAV_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
Pprof bool `yaml:"pprof" env:"OCDAV_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
Zpages bool `yaml:"zpages" env:"OCDAV_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
}
type HTTPConfig struct {
Addr string `yaml:"addr" env:"OCDAV_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"`
Namespace string `yaml:"-"`
Protocol string `yaml:"protocol" env:"OCDAV_HTTP_PROTOCOL" desc:"The transport protocol of the HTTP service." introductionVersion:"1.0.0"`
Prefix string `yaml:"prefix" env:"OCDAV_HTTP_PREFIX" desc:"A URL path prefix for the handler." introductionVersion:"1.0.0"`
CORS `yaml:"cors"`
}
// CORS defines the available cors configuration.
type CORS struct {
AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;OCDAV_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;OCDAV_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;OCDAV_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;OCDAV_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"1.0.0"`
}
// Status holds the configurable values for the status.php
type Status struct {
Version string
VersionString string
Product string
ProductName string
ProductVersion string
Edition string `desc:"Edition of OpenCloud. Used for branding purposes." introductionVersion:"1.0.0"`
}

View File

@@ -1,141 +0,0 @@
package defaults
import (
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/pkg/version"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
)
// FullDefaultConfig returns a fully initialized default configuration
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig returns a basic default configuration
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9163",
Token: "",
Pprof: false,
Zpages: false,
},
HTTP: config.HTTPConfig{
Addr: "127.0.0.1:9350",
Namespace: "eu.opencloud.web",
Protocol: "tcp",
Prefix: "",
CORS: config.CORS{
AllowedOrigins: []string{"https://localhost:9200"},
AllowedMethods: []string{
"OPTIONS",
"HEAD",
"GET",
"PUT",
"POST",
"DELETE",
"MKCOL",
"PROPFIND",
"PROPPATCH",
"MOVE",
"COPY",
"REPORT",
"SEARCH",
},
AllowedHeaders: []string{
"Origin",
"Accept",
"Content-Type",
"Depth",
"Authorization",
"Ocs-Apirequest",
"If-None-Match",
"If-Match",
"Destination",
"Overwrite",
"X-Request-Id",
"X-Requested-With",
"Tus-Resumable",
"Tus-Checksum-Algorithm",
"Upload-Concat",
"Upload-Length",
"Upload-Metadata",
"Upload-Defer-Length",
"Upload-Expires",
"Upload-Checksum",
"Upload-Offset",
"X-HTTP-Method-Override",
"Cache-Control",
},
AllowCredentials: false,
},
},
Service: config.Service{
Name: "ocdav",
},
Reva: shared.DefaultRevaConfig(),
WebdavNamespace: "/users/{{.Id.OpaqueId}}",
FilesNamespace: "/users/{{.Id.OpaqueId}}",
SharesNamespace: "/Shares",
OCMNamespace: "/public",
PublicURL: "https://localhost:9200",
Insecure: false,
Timeout: 84300,
MachineAuthAPIKey: "",
Status: config.Status{
Version: version.Legacy,
VersionString: version.LegacyString,
ProductVersion: version.GetString(),
Product: "OpenCloud",
ProductName: "OpenCloud",
Edition: version.Edition,
},
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for "envdecode".
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
if cfg.Reva == nil && cfg.Commons != nil {
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
}
if (cfg.Commons != nil && cfg.Commons.OpenCloudURL != "") &&
(cfg.HTTP.CORS.AllowedOrigins == nil ||
len(cfg.HTTP.CORS.AllowedOrigins) == 1 &&
cfg.HTTP.CORS.AllowedOrigins[0] == "https://localhost:9200") {
cfg.HTTP.CORS.AllowedOrigins = []string{cfg.Commons.OpenCloudURL}
}
}
// Sanitize sanitizes the configuration
func Sanitize(cfg *config.Config) {
// nothing to sanitize here atm
}

View File

@@ -1,50 +0,0 @@
package parser
import (
"errors"
occfg "github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
return Validate(cfg)
}
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
if cfg.MachineAuthAPIKey == "" {
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
}
if cfg.Commons.URLSigningSecret == "" {
return shared.MissingURLSigningSecret(cfg.Service.Name)
}
return nil
}

View File

@@ -1,6 +0,0 @@
package config
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;OCDAV_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
}

View File

@@ -1,17 +0,0 @@
package logging
import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
)
// Configure initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}

View File

@@ -1,50 +0,0 @@
package debug
import (
"context"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}

View File

@@ -1,27 +0,0 @@
package debug
import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/service/debug"
"github.com/opencloud-eu/opencloud/pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
//debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
//debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
//debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
//debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
), nil
}

View File

@@ -164,4 +164,4 @@ A good example of how such a file should be formatted can be found in the [Apach
## Example Policies
The policies service contains a set of preconfigured example policies. See the [devtools policie](https://github.com/opencloud-eu/opencloud/tree/main/devtools/deployments/service_policies/policies/) directory for details. The contained policies disallow OpenCloud to create certain file types, both via the proxy middleware and the events service via postprocessing.
The policies service contains a set of preconfigured example policies. See the [deployment examples](https://github.com/opencloud-eu/opencloud/tree/main/deployments/examples) directory for details. The contained policies disallow OpenCloud to create certain file types, both via the proxy middleware and the events service via postprocessing.

View File

@@ -28,7 +28,7 @@ policies:
- endpoint: /
service: eu.opencloud.web.web
- endpoint: /dav/
service: eu.opencloud.web.ocdav
service: eu.opencloud.web.frontend
```
For adding _additional_ routes to the default routes use:

View File

@@ -223,33 +223,33 @@ func DefaultPolicies() []config.Policy {
},
{
Endpoint: "/remote.php/",
Service: "eu.opencloud.web.ocdav",
Service: "eu.opencloud.web.frontend",
},
{
Endpoint: "/dav/",
Service: "eu.opencloud.web.ocdav",
Service: "eu.opencloud.web.frontend",
},
{
Endpoint: "/webdav/",
Service: "eu.opencloud.web.ocdav",
Service: "eu.opencloud.web.frontend",
},
{
Endpoint: "/status",
Service: "eu.opencloud.web.ocdav",
Service: "eu.opencloud.web.frontend",
Unprotected: true,
},
{
Endpoint: "/status.php",
Service: "eu.opencloud.web.ocdav",
Service: "eu.opencloud.web.frontend",
Unprotected: true,
},
{
Endpoint: "/index.php/",
Service: "eu.opencloud.web.ocdav",
Service: "eu.opencloud.web.frontend",
},
{
Endpoint: "/apps/",
Service: "eu.opencloud.web.ocdav",
Service: "eu.opencloud.web.frontend",
},
{
Endpoint: "/data",
@@ -262,7 +262,7 @@ func DefaultPolicies() []config.Policy {
Unprotected: true,
},
{
Endpoint: "/app/", // /app or /apps? ocdav only handles /apps
Endpoint: "/app/", // /app or /apps? frontend only handles /apps
Service: "eu.opencloud.web.frontend",
},
{

View File

@@ -7,5 +7,5 @@ type HTTP struct {
Namespace string `yaml:"-"`
TLSCert string `yaml:"tls_cert" env:"PROXY_TRANSPORT_TLS_CERT" desc:"Path/File name of the TLS server certificate (in PEM format) for the external http services. If not defined, the root directory derives from $OC_BASE_DATA_PATH/proxy." introductionVersion:"1.0.0"`
TLSKey string `yaml:"tls_key" env:"PROXY_TRANSPORT_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the external http services. If not defined, the root directory derives from $OC_BASE_DATA_PATH/proxy." introductionVersion:"1.0.0"`
TLS bool `yaml:"tls" env:"PROXY_TLS" desc:"Enable/Disable HTTPS for external HTTP services. Must be set to 'true' if the built-in IDP service and no reverse proxy is used. See the text description for details." introductionVersion:"1.0.0"`
TLS bool `yaml:"tls" env:"PROXY_TLS" desc:"Enable/Disable HTTPS for external HTTP services. Must be set to 'true' if the built-in IDP service an no reverse proxy is used. See the text description for details." introductionVersion:"1.0.0"`
}

View File

@@ -127,7 +127,7 @@ func TestRouter(t *testing.T) {
Name: "default",
Routes: []config.Route{
{Type: config.PrefixRoute, Endpoint: "/web/unprotected/demo/", Backend: "http://web", Unprotected: true},
{Type: config.PrefixRoute, Endpoint: "/dav", Backend: "http://ocdav"},
{Type: config.PrefixRoute, Endpoint: "/dav", Backend: "http://frontend"},
{Type: config.PrefixRoute, Method: "REPORT", Endpoint: "/dav", Backend: "http://opencloud-webdav"},
},
},
@@ -138,7 +138,7 @@ func TestRouter(t *testing.T) {
router := New(sel, policySelectorCfg, policies, log.NewLogger())
table := []matchertest{
{method: "PROPFIND", endpoint: "/dav/files/demo/", target: "ocdav"},
{method: "PROPFIND", endpoint: "/dav/files/demo/", target: "frontend"},
{method: "REPORT", endpoint: "/dav/files/demo/", target: "opencloud-webdav"},
{method: "GET", endpoint: "/web/unprotected/demo/", target: "web", unprotected: true},
}

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-14 00:02+0000\n"
"POT-Creation-Date: 2025-11-24 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/opencloud-eu/teams/204053/fi/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-18 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: idoet <idoet@protonmail.ch>, 2025\n"
"Language-Team: Indonesian (https://app.transifex.com/opencloud-eu/teams/204053/id/)\n"

View File

@@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-15 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/opencloud-eu/teams/204053/pl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-17 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -12,7 +12,7 @@ This service is part of the storage services family and is responsible for:
The storage-publiclink service integrates with:
- `sharing` service - Manages and persists public link shares
- `frontend` and `ocdav` - Provide HTTP/WebDAV access to public links
- `frontend` service - Provides HTTP/WebDAV access to public links
- Storage drivers - Accesses the actual file content
## Storage Registry

View File

@@ -13,7 +13,7 @@ This service is part of the storage services family and is responsible for:
The storage-shares service integrates with:
- `sharing` service - Manages and persists shares
- `storage-users` service - Accesses the underlying file content
- `frontend` and `ocdav` - Provide HTTP/WebDAV access to shares
- `frontend` service - Provides HTTP/WebDAV access to shares
## Virtual Shares Folder

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-15 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/opencloud-eu/teams/204053/nl/)\n"

View File

@@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-15 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/opencloud-eu/teams/204053/pl/)\n"

View File

@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2025-12-13 00:02+0000\n"
"POT-Creation-Date: 2025-11-17 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/opencloud-eu/teams/204053/pt/)\n"

View File

@@ -1,6 +1,6 @@
SHELL := bash
NAME := web
WEB_ASSETS_VERSION = v4.3.0
WEB_ASSETS_VERSION = v4.2.1
WEB_ASSETS_BRANCH = main
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI

View File

@@ -136,9 +136,6 @@ func (p Web) getPayload() (payload []byte, err error) {
p.config.Web.Config.Apps = make([]string, 0)
}
// ensure that the server url has a trailing slash
p.config.Web.Config.Server = strings.TrimRight(p.config.Web.Config.Server, "/") + "/"
return json.Marshal(p.config.Web.Config)
}

View File

@@ -1,6 +1,6 @@
# Webdav
The webdav service, like the [ocdav](../ocdav) service, provides a HTTP API following the webdav protocol. It receives HTTP calls from requestors like clients and issues gRPC calls to other services executing these requests. After the called service has finished the request, the webdav service will render their responses in `xml` and sends them back to the requestor.
The webdav service, like the [frontend](../frontend) service, provides a HTTP API following the webdav protocol. It receives HTTP calls from requestors like clients and issues gRPC calls to other services executing these requests. After the called service has finished the request, the webdav service will render their responses in `xml` and sends them back to the requestor.
## Endpoints Overview

View File

@@ -33,7 +33,6 @@ use SimpleXMLElement;
use Sabre\Xml\LibXMLException;
use Sabre\Xml\Reader;
use GuzzleHttp\Pool;
use Symfony\Component\HttpFoundation\Response;
/**
* Helper for HTTP requests
@@ -75,6 +74,7 @@ class HttpRequestHelper {
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
* @param string|null $bearerToken
*
* @return ResponseInterface
* @throws GuzzleException
@@ -92,42 +92,8 @@ class HttpRequestHelper {
bool $stream = false,
?int $timeout = 0,
?Client $client = null,
?string $bearerToken = null
): ResponseInterface {
$bearerToken = null;
if (TokenHelper::useBearerToken() && $user && $user !== 'public') {
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
// check token is still valid
$parsedUrl = parse_url($url);
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
$baseUrl .= isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
$testUrl = $baseUrl . "/graph/v1.0/use/$user";
if (OcHelper::isTestingOnReva()) {
$url = $baseUrl . "/ocs/v2.php/cloud/users/$user";
}
// check token validity with a GET request
$c = self::createClient(
$user,
$password,
$config,
$cookies,
$stream,
$timeout,
$bearerToken
);
$testReq = self::createRequest($testUrl, $xRequestId, 'GET');
try {
$testRes = $c->send($testReq);
} catch (RequestException $ex) {
$testRes = $ex->getResponse();
if ($testRes && $testRes->getStatusCode() === Response::HTTP_UNAUTHORIZED) {
// token is invalid or expired, get a new one
echo "[INFO] Bearer token expired or invalid, getting a new one...\n";
TokenHelper::clearAllTokens();
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
}
}
}
if ($client === null) {
$client = self::createClient(
$user,
@@ -194,24 +160,6 @@ class HttpRequestHelper {
}
HttpLogger::logResponse($response);
// wait for post-processing to finish if applicable
if (WebdavHelper::isDAVRequest($url)
&& \str_starts_with($url, OcHelper::getServerUrl())
&& \in_array($method, ["PUT", "MOVE", "COPY"])
&& \in_array($response->getStatusCode(), [Response::HTTP_CREATED, Response::HTTP_NO_CONTENT])
&& OcConfigHelper::getPostProcessingDelay() === 0
) {
if (\in_array($method, ["MOVE", "COPY"])) {
$url = $headers['Destination'];
}
WebDavHelper::waitForPostProcessingToFinish(
$url,
$user,
$password,
$headers,
);
}
return $response;
}
@@ -255,6 +203,13 @@ class HttpRequestHelper {
} else {
$debugResponses = false;
}
// use basic auth for 'public' user or no user
if ($user === 'public' || $user === null || $user === '') {
$bearerToken = null;
} else {
$useBearerToken = TokenHelper::useBearerToken();
$bearerToken = $useBearerToken ? TokenHelper::getTokens($user, $password, $url)['access_token'] : null;
}
$sendRetryLimit = self::numRetriesOnHttpTooEarly();
$sendCount = 0;
@@ -273,6 +228,7 @@ class HttpRequestHelper {
$stream,
$timeout,
$client,
$bearerToken,
);
if ($response->getStatusCode() >= 400
@@ -300,8 +256,7 @@ class HttpRequestHelper {
// we need to repeat the send request, because we got HTTP_TOO_EARLY or HTTP_CONFLICT
// wait 1 second before sending again, to give the server some time
// to finish whatever post-processing it might be doing.
echo "[INFO] Received '" . $response->getStatusCode() .
"' status code, retrying request ($sendCount)...\n";
self::debugResponse($response);
\sleep(1);
}
} while ($loopAgain);

View File

@@ -30,26 +30,6 @@ use Psr\Http\Message\ResponseInterface;
* A helper class for configuring OpenCloud server
*/
class OcConfigHelper {
public static $postProcessingDelay = 0;
/**
* @return int
*/
public static function getPostProcessingDelay(): int {
return self::$postProcessingDelay;
}
/**
* @param string $postProcessingDelay
*
* @return void
*/
public static function setPostProcessingDelay(string $postProcessingDelay): void {
// extract number from string
$delay = (int) filter_var($postProcessingDelay, FILTER_SANITIZE_NUMBER_INT);
self::$postProcessingDelay = $delay;
}
/**
* @param string $url
* @param string $method

View File

@@ -84,9 +84,7 @@ class TokenHelper {
$tokenData = [
'access_token' => $refreshedToken['access_token'],
'refresh_token' => $refreshedToken['refresh_token'],
// set expiry to 240 (4 minutes) seconds to allow for some buffer
// token actually expires in 300 seconds (5 minutes)
'expires_at' => time() + 240
'expires_at' => time() + 300 // 5 minutes
];
self::$tokenCache[$cacheKey] = $tokenData;
return $tokenData;
@@ -102,9 +100,7 @@ class TokenHelper {
$tokenData = [
'access_token' => $tokens['access_token'],
'refresh_token' => $tokens['refresh_token'],
// set expiry to 240 (4 minutes) seconds to allow for some buffer
// token actually expires in 300 seconds (5 minutes)
'expires_at' => time() + 240
'expires_at' => time() + 290 // set expiry to 290 seconds to allow for some buffer
];
// Save to cache

View File

@@ -923,45 +923,4 @@ class WebDavHelper {
$mtime = new DateTime($xmlPart[0]->__toString());
return $mtime->format('U');
}
/**
* wait until the reqeust doesn't return 425 anymore
*
* @param string $url
* @param ?string $user
* @param ?string $password
* @param ?array $headers
*
* @return void
*/
public static function waitForPostProcessingToFinish(
string $url,
?string $user = null,
?string $password = null,
?array $headers = [],
): void {
$retried = 0;
do {
$response = HttpRequestHelper::sendRequest(
$url,
'check-425-status',
'GET',
$user,
$password,
$headers,
);
$statusCode = $response->getStatusCode();
if ($statusCode !== 425) {
return;
}
$tryAgain = $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "[INFO] Waiting for post processing to finish, attempt ($retried)...\n";
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
echo "[ERROR] 10 seconds timeout! Post processing did not finish in time.\n";
}
}

View File

@@ -2026,12 +2026,8 @@ class FeatureContext extends BehatVariablesContext {
if ($response === null) {
$response = $this->getResponse();
}
$body = (string)$response->getBody();
if (!$body) {
return [];
}
return \json_decode(
$body,
(string)$response->getBody(),
true
);
}

View File

@@ -68,7 +68,6 @@ class OcConfigContext implements Context {
$response->getStatusCode(),
"Failed to set async upload with delayed post processing"
);
OcConfigHelper::setPostProcessingDelay($delayTime);
}
/**
@@ -91,9 +90,6 @@ class OcConfigContext implements Context {
$response->getStatusCode(),
"Failed to set config $configVariable=$configValue"
);
if ($configVariable === "POSTPROCESSING_DELAY") {
OcConfigHelper::setPostProcessingDelay($configValue);
}
}
/**
@@ -188,9 +184,6 @@ class OcConfigContext implements Context {
$envs = [];
foreach ($table->getHash() as $row) {
$envs[$row['config']] = $row['value'];
if ($row['config'] === "POSTPROCESSING_DELAY") {
OcConfigHelper::setPostProcessingDelay($row['value']);
}
}
$response = OcConfigHelper::reConfigureOc($envs);
@@ -207,7 +200,6 @@ class OcConfigContext implements Context {
* @return void
*/
public function rollbackOc(): void {
OcConfigHelper::setPostProcessingDelay('0');
$response = OcConfigHelper::rollbackOc();
Assert::assertEquals(
200,

View File

@@ -607,7 +607,7 @@ trait Provisioning {
Assert::assertEquals(
201,
$response->getStatusCode(),
__METHOD__ . " cannot create user '$userName'.\nResponse:" .
__METHOD__ . " cannot create user '$userName' using Graph API.\nResponse:" .
json_encode($this->getJsonDecodedResponse($response))
);
@@ -1083,7 +1083,7 @@ trait Provisioning {
Assert::assertEquals(
201,
$response->getStatusCode(),
__METHOD__ . " cannot create user '$user'.\nResponse:" .
__METHOD__ . " cannot create user '$user' using Graph API.\nResponse:" .
json_encode($this->getJsonDecodedResponse($response))
);
$userId = $this->getJsonDecodedResponse($response)['id'];

View File

@@ -750,9 +750,6 @@ class SpacesContext implements Context {
} else {
$rawBody = $this->featureContext->getResponse()->getBody()->getContents();
}
if (!$rawBody) {
throw new Exception(__METHOD__ . " - Response body is empty");
}
$drives = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
if (isset($drives["value"])) {
$drives = $drives["value"];

View File

@@ -216,44 +216,6 @@ class TUSContext implements Context {
);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data with retry on offset mismatch using the WebDAV API
*
* @param string $user
* @param string $offset
* @param string $data
*
* @return void
*
* @throws GuzzleException
* @throws JsonException
*/
public function userSendsAChunkToTUSLocationWithOffsetAndDataWithRetryOnOffsetMismatch(
string $user,
string $offset,
string $data,
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$retried = 0;
do {
$tryAgain = false;
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data);
// retry on 409 Conflict (Offset mismatch during TUS upload)
if ($response->getStatusCode() === 409) {
$tryAgain = true;
}
$tryAgain = $tryAgain && $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "Offset mismatch during TUS upload, retrying ($retried)...\n";
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
$this->featureContext->setResponse($response);
}
/**
* @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data using the WebDAV API
*

View File

@@ -25,7 +25,6 @@ use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Stream\StreamInterface;
use TestHelpers\OcConfigHelper;
use TestHelpers\OcHelper;
use TestHelpers\UploadHelper;
use TestHelpers\WebDavHelper;
@@ -744,7 +743,6 @@ trait WebDav {
/**
* @When the user waits for :time seconds for postprocessing to finish
* @When the user waits for :time seconds
*
* @param int $time
*
@@ -975,61 +973,6 @@ trait WebDav {
$this->checkDownloadedContentMatches($content, '', $response);
}
/**
* check file content with retry
*
* @param string $user
* @param string $fileName
* @param string $content
*
* @return void
* @throws Exception
*/
public function checkFileContentWithRetry(string $user, string $fileName, string $content): void {
$retried = 0;
do {
$tryAgain = false;
$response = $this->downloadFileAsUserUsingPassword($this->getActualUsername($user), $fileName);
$status = $response->getStatusCode();
$downloadedContent = $response->getBody()->getContents();
if ($status !== 200) {
$tryAgain = true;
$message = "Expected '200' but got '$status'";
} elseif ($downloadedContent !== $content) {
$tryAgain = true;
$message = "Expected content '$content' but got '$downloadedContent'";
}
$tryAgain = $tryAgain && $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "[INFO] File content mismatch. $message, checking content again ($retried)...\n";
// break the loop if status is 425 as the request will already be retried
if ($status === HttpRequestHelper::HTTP_TOO_EARLY) {
break;
}
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
$this->theHTTPStatusCodeShouldBe(200, '', $response);
$this->checkDownloadedContentMatches($content, '', $response);
}
/**
* @Then as :user the final content of file :fileName should be :content
*
* @param string $user
* @param string $fileName
* @param string $content
*
* @return void
*/
public function asUserFinalContentOfFileShouldBe(string $user, string $fileName, string $content): void {
$this->checkFileContentWithRetry($user, $fileName, $content);
}
/**
* @Then /^the content of the following files for user "([^"]*)" should be "([^"]*)"$/
*
@@ -1321,18 +1264,20 @@ trait WebDav {
$type
);
$statusCode = $response->getStatusCode();
// when checking path with '..' it may return 405 Method Not Allowed
if ($statusCode === 404 || $statusCode === 405) {
return;
}
if ($statusCode === 207) {
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
if ($statusCode < 400 || $statusCode > 499) {
try {
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
} catch (Exception $e) {
Assert::fail(
"$entry '$path' should not exist. But API returned $statusCode without XML in the body"
);
}
Assert::assertTrue(
$this->isEtagValid($this->getEtagFromResponseXmlObject($responseXmlObject)),
"$entry '$path' should not exist but found with invalid etag."
"$entry '$path' should not exist. But API returned $statusCode without an etag in the body"
);
$isCollection = $responseXmlObject->xpath("//d:prop/d:resourcetype/d:collection");
if (\count($isCollection) === 0) {
@@ -1346,11 +1291,7 @@ trait WebDav {
"$entry '$path' should not exist. But it does."
);
}
return;
}
Assert::fail(
"$entry '$path' should not exist. But API returned $statusCode without XML in the body"
);
}
/**
@@ -2329,11 +2270,6 @@ trait WebDav {
"HTTP status code was not 201 or 204 while trying to upload file '$destination' for user '$user'",
$response
);
// check uploaded content only if post-processing delay is not configured
if (OcConfigHelper::getPostProcessingDelay() === 0) {
$this->checkFileContentWithRetry($user, $destination, $content);
}
return $response->getHeader('oc-fileid');
}

View File

@@ -190,7 +190,7 @@
#### [Readiness check for some services returns 500 status code](https://github.com/owncloud/ocis/issues/10661)
- [apiServiceAvailability/serviceAvailabilityCheck.feature:125](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L125)
- [apiServiceAvailability/serviceAvailabilityCheck.feature:123](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L123)
#### [Skip tests for different languages](https://github.com/opencloud-eu/opencloud/issues/183)
- [apiActivities/activities.feature:2598](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiActivities/activities.feature#L2598)

View File

@@ -190,7 +190,7 @@
#### [Readiness check for some services returns 500 status code](https://github.com/owncloud/ocis/issues/10661)
- [apiServiceAvailability/serviceAvailabilityCheck.feature:125](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L125)
- [apiServiceAvailability/serviceAvailabilityCheck.feature:123](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiServiceAvailability/serviceAvailabilityCheck.feature#L123)
#### [Skip tests for different languages](https://github.com/opencloud-eu/opencloud/issues/183)
- [apiActivities/activities.feature:2598](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiActivities/activities.feature#L2598)

View File

@@ -19,7 +19,6 @@ Feature: service health check
| http://%base_url_hostname%:9239/healthz | idm |
| http://%base_url_hostname%:9134/healthz | idp |
| http://%base_url_hostname%:9234/healthz | nats |
| http://%base_url_hostname%:9163/healthz | ocdav |
| http://%base_url_hostname%:9281/healthz | ocm |
| http://%base_url_hostname%:9114/healthz | ocs |
| http://%base_url_hostname%:9255/healthz | postprocessing |
@@ -74,7 +73,6 @@ Feature: service health check
| http://%base_url_hostname%:9161/readyz | groups |
| http://%base_url_hostname%:9239/readyz | idm |
| http://%base_url_hostname%:9234/readyz | nats |
| http://%base_url_hostname%:9163/readyz | ocdav |
| http://%base_url_hostname%:9281/readyz | ocm |
| http://%base_url_hostname%:9114/readyz | ocs |
| http://%base_url_hostname%:9255/readyz | postprocessing |

View File

@@ -29,7 +29,7 @@ Feature: create a resources using collaborative posixfs
Scenario: create file
When the administrator creates the file "test.txt" with content "content" for user "Alice" on the POSIX filesystem
Then the command should be successful
And as "Alice" the final content of file "test.txt" should be "content"
And the content of file "/test.txt" for user "Alice" should be "content"
Scenario: create large file
@@ -41,22 +41,21 @@ Feature: create a resources using collaborative posixfs
Scenario: creates files sequentially in a folder
When the administrator creates 50 files sequentially in the directory "firstFolder" for user "Alice" on the POSIX filesystem
Then the command should be successful
And as "Alice" the final content of file "/firstFolder/file_1.txt" should be "file 1 content"
And as "Alice" the final content of file "/firstFolder/file_50.txt" should be "file 50 content"
And the content of file "/firstFolder/file_1.txt" for user "Alice" should be "file 1 content"
And the content of file "/firstFolder/file_50.txt" for user "Alice" should be "file 50 content"
Scenario: creates files in parallel in a folder
When the administrator creates 100 files in parallel in the directory "firstFolder" for user "Alice" on the POSIX filesystem
Then the command should be successful
And as "Alice" the final content of file "/firstFolder/parallel_1.txt" should be "parallel file 1 content"
And as "Alice" the final content of file "/firstFolder/parallel_100.txt" should be "parallel file 100 content"
And the content of file "/firstFolder/parallel_1.txt" for user "Alice" should be "parallel file 1 content"
And the content of file "/firstFolder/parallel_100.txt" for user "Alice" should be "parallel file 100 content"
Scenario: edit file
Given user "Alice" has uploaded file with content "content" to "test.txt"
When the administrator puts the content "new" into the file "test.txt" in the POSIX storage folder of user "Alice"
Then the command should be successful
And as "Alice" the final content of file "test.txt" should be "contentnew"
Then the content of file "/test.txt" for user "Alice" should be "contentnew"
Scenario: read file content
@@ -69,14 +68,14 @@ Feature: create a resources using collaborative posixfs
Given user "Alice" has uploaded file with content "content" to "test.txt"
When the administrator copies the file "test.txt" to the folder "firstFolder" for user "Alice" on the POSIX filesystem
Then the command should be successful
And as "Alice" the final content of file "/firstFolder/test.txt" should be "content"
And the content of file "/firstFolder/test.txt" for user "Alice" should be "content"
Scenario: rename file
Given user "Alice" has uploaded file with content "content" to "test.txt"
When the administrator renames the file "test.txt" to "new-name.txt" for user "Alice" on the POSIX filesystem
Then the command should be successful
And as "Alice" the final content of file "/new-name.txt" should be "content"
And the content of file "/new-name.txt" for user "Alice" should be "content"
Scenario: check propfind after rename file
@@ -98,14 +97,14 @@ Feature: create a resources using collaborative posixfs
Given the administrator has created the file "test.txt" with content "content" for user "Alice" on the POSIX filesystem
When the administrator renames the file "test.txt" to "test.md" for user "Alice" on the POSIX filesystem
Then the command should be successful
And as "Alice" the final content of file "/test.md" should be "content"
And the content of file "/test.md" for user "Alice" should be "content"
Scenario: move file to folder
Given user "Alice" has uploaded file with content "content" to "test.txt"
When the administrator moves the file "test.txt" to the folder "firstFolder" for user "Alice" on the POSIX filesystem
Then the command should be successful
And as "Alice" the final content of file "/firstFolder/test.txt" should be "content"
And the content of file "/firstFolder/test.txt" for user "Alice" should be "content"
And as "Alice" file "/test.txt" should not exist
@@ -203,4 +202,4 @@ Feature: create a resources using collaborative posixfs
And the administrator renames the file "test.txt" to "renamed.txt" for user "Alice" on the POSIX filesystem
And the administrator checks the attribute "user.oc.name" of file "renamed.txt" for user "Alice" on the POSIX filesystem
Then the command output should contain "renamed.txt"
And as "Alice" the final content of file "/renamed.txt" should be "content"
And the content of file "/renamed.txt" for user "Alice" should be "content"

View File

@@ -202,7 +202,7 @@ Feature: capabilities
"properties": {
"edition": {
"type": "string",
"enum": ["dev"]
"enum": ["%edition%"]
},
"product": {
"type": "string",
@@ -240,7 +240,7 @@ Feature: capabilities
},
"edition": {
"type": "string",
"enum": ["dev"]
"enum": ["%edition%"]
},
"product": {
"type": "string",

View File

@@ -58,7 +58,7 @@ Feature: default capabilities for normal user
"const": "%versionstring%"
},
"edition": {
"const": "dev"
"const": "%edition%"
},
"productname": {
"const": "%productname%"

View File

@@ -50,7 +50,8 @@ Feature: low level tests for upload of chunks
| Upload-Metadata | filename ZmlsZS50eHQ= |
When user "Alice" sends a chunk to the last created TUS Location with offset "0" and data "123" using the WebDAV API
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "4567890" using the WebDAV API
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "0000000" with retry on offset mismatch using the WebDAV API
And the user waits for "2" seconds for postprocessing to finish
And user "Alice" sends a chunk to the last created TUS Location with offset "3" and data "0000000" using the WebDAV API
Then the HTTP status code should be "404"
And the content of file "/file.txt" for user "Alice" should be "1234567890"
Examples:
@@ -60,22 +61,6 @@ Feature: low level tests for upload of chunks
| spaces |
Scenario Outline: send last chunk with mismatch offset
Given using <dav-path-version> DAV path
And user "Alice" has created a new TUS resource on the WebDAV API with these headers:
| Upload-Length | 10 |
# ZmlsZS50eHQ= is the base64 encode of file.txt
| Upload-Metadata | filename ZmlsZS50eHQ= |
When user "Alice" sends a chunk to the last created TUS Location with offset "0" and data "123" using the WebDAV API
And user "Alice" sends a chunk to the last created TUS Location with offset "2" and data "34567890" using the WebDAV API
Then the HTTP status code should be "409"
Examples:
| dav-path-version |
| old |
| new |
| spaces |
Scenario Outline: start with uploading not at the beginning of the file
Given using <dav-path-version> DAV path
And user "Alice" has created a new TUS resource on the WebDAV API with these headers:

View File

@@ -45,8 +45,6 @@ export IDP_DEBUG_ADDR=127.0.0.1:10134
export IDP_HTTP_ADDR=127.0.0.1:10130
export NATS_DEBUG_ADDR=127.0.0.1:10234
export NATS_NATS_PORT=10233
export OCDAV_HTTP_ADDR=127.0.0.1:10350
export OCDAV_DEBUG_ADDR=127.0.0.1:10163
export OCM_DEBUG_ADDR=127.0.0.1:10281
export OCM_HTTP_ADDR=127.0.0.1:10280
export OCM_GRPC_ADDR=127.0.0.1:10282

View File

@@ -46,27 +46,10 @@ type Options struct {
WatchRoot string `mapstructure:"watch_root"` // base directory for the watch. events will be considered relative to this path
WatchNotificationBrokers string `mapstructure:"watch_notification_brokers"`
NatsWatcher NatsWatcherConfig `mapstructure:"natswatcher"`
// InotifyWatcher specific options
InotifyStatsFrequency time.Duration `mapstructure:"inotify_stats_frequency"`
}
// NatsWatcherConfig is the configuration needed for a NATS watcher event stream.
type NatsWatcherConfig struct {
Endpoint string `mapstructure:"address"`
Cluster string `mapstructure:"clusterID"`
Stream string `mapstructure:"stream"`
Durable string `mapstructure:"durable-name"`
TLSInsecure bool `mapstructure:"tls-insecure"`
TLSRootCACertificate string `mapstructure:"tls-root-ca-cert"`
EnableTLS bool `mapstructure:"enable-tls"`
AuthUsername string `mapstructure:"username"`
AuthPassword string `mapstructure:"password"`
MaxAckPending int `mapstructure:"max-ack-pending"`
AckWait time.Duration `mapstructure:"ack-wait"`
}
// New returns a new Options instance for the given configuration
func New(m map[string]interface{}) (*Options, error) {
// default to hybrid metadatabackend for posixfs

View File

@@ -21,7 +21,6 @@ package trashbin
import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
@@ -590,10 +589,6 @@ func (tb *Trashbin) IsEmpty(ctx context.Context, spaceID string) bool {
}
dirItems, err := trash.ReadDir(1)
if err != nil {
if err == io.EOF {
// empty trash
return true
}
// if we cannot read the trash, we assume there are no trashed items
tb.log.Error().Err(err).Str("spaceID", spaceID).Msg("trashbin: error reading trash directory")
return true

View File

@@ -39,7 +39,6 @@ import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node"
@@ -55,6 +54,16 @@ type ScanDebouncer struct {
mutex sync.Mutex
}
type EventAction int
const (
ActionCreate EventAction = iota
ActionUpdate
ActionMove
ActionDelete
ActionMoveFrom
)
type queueItem struct {
item scanItem
timer *time.Timer
@@ -181,10 +190,10 @@ func (t *Tree) workScanQueue() {
}
// Scan scans the given path and updates the id chache
func (t *Tree) Scan(path string, action watcher.EventAction, isDir bool) error {
func (t *Tree) Scan(path string, action EventAction, isDir bool) error {
// cases:
switch action {
case watcher.ActionCreate:
case ActionCreate:
t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionCreate)")
if !isDir {
// 1. New file (could be emitted as part of a new directory)
@@ -216,7 +225,7 @@ func (t *Tree) Scan(path string, action watcher.EventAction, isDir bool) error {
})
}
case watcher.ActionUpdate:
case ActionUpdate:
t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionUpdate)")
// 3. Updated file
// -> update file unless parent directory is being rescanned
@@ -232,7 +241,7 @@ func (t *Tree) Scan(path string, action watcher.EventAction, isDir bool) error {
AssimilationCounter.WithLabelValues(_labelDir, _labelUpdated).Inc()
}
case watcher.ActionMove:
case ActionMove:
t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionMove)")
// 4. Moved file
// -> update file
@@ -249,7 +258,7 @@ func (t *Tree) Scan(path string, action watcher.EventAction, isDir bool) error {
AssimilationCounter.WithLabelValues(_labelDir, _labelMoved).Inc()
}
case watcher.ActionMoveFrom:
case ActionMoveFrom:
t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionMoveFrom)")
// 6. file/directory moved out of the watched directory
// -> remove from caches
@@ -270,7 +279,7 @@ func (t *Tree) Scan(path string, action watcher.EventAction, isDir bool) error {
// We do not do metrics here because this has been handled in `ActionMove`
case watcher.ActionDelete:
case ActionDelete:
t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("handling deleted item")
// 7. Deleted file or directory
@@ -417,15 +426,6 @@ func (t *Tree) assimilate(item scanItem) error {
}
}
fi, err := os.Lstat(item.Path)
if err != nil {
return err
}
if !fi.IsDir() && !fi.Mode().IsRegular() {
t.log.Trace().Str("path", item.Path).Msg("skipping non-regular file")
return nil
}
if id != "" {
// the file has an id set, we already know it from the past
@@ -451,10 +451,20 @@ func (t *Tree) assimilate(item scanItem) error {
// compare metadata mtime with actual mtime. if it matches AND the path hasn't changed (move operation)
// we can skip the assimilation because the file was handled by us
fi, err := os.Lstat(item.Path)
if err != nil {
return err
}
if previousPath == item.Path && mtime.Equal(fi.ModTime()) {
return nil
}
if !fi.IsDir() && !fi.Mode().IsRegular() {
t.log.Trace().Str("path", item.Path).Msg("skipping non-regular file")
return nil
}
// was it moved or copied/restored with a clashing id?
if ok && len(parentID) > 0 && previousPath != item.Path {
_, err := os.Stat(previousPath)
@@ -665,7 +675,6 @@ assimilate:
}
var n *node.Node
sizeDiff := int64(0)
if fi.IsDir() {
// The Space's name attribute might not match the directory name. Use the name as
// it was set before. Also the space root doesn't have a 'type' attribute
@@ -703,46 +712,44 @@ assimilate:
n.SpaceRoot = &node.Node{BaseNode: node.BaseNode{SpaceID: spaceID, ID: spaceID}}
prevBlobSize, err := previousAttribs.Int64(prefixes.BlobsizeAttr)
if err != nil || prevBlobSize < 0 {
prevBlobSize = 0
}
if prevBlobSize != fi.Size() {
sizeDiff = fi.Size() - prevBlobSize
if err == nil && prevBlobSize != fi.Size() {
// file size changed, trigger propagation of tree size changes
err = t.Propagate(context.Background(), n, fi.Size()-prevBlobSize)
if err != nil {
t.log.Error().Err(err).Str("path", path).Msg("could not propagate tree size changes")
}
}
}
attributes.SetTime(prefixes.MTimeAttr, fi.ModTime())
n.SpaceRoot = &node.Node{BaseNode: node.BaseNode{SpaceID: spaceID, ID: spaceID}}
if !fi.IsDir() && t.options.EnableFSRevisions {
if t.options.EnableFSRevisions {
go func() {
// Copy the previous current version to a revision
currentNode := node.NewBaseNode(n.SpaceID, n.ID+node.CurrentIDDelimiter, t.lookup)
currentPath := currentNode.InternalPath()
stat, err := os.Stat(currentPath)
if err == nil {
revisionPath := t.lookup.VersionPath(n.SpaceID, n.ID, stat.ModTime().UTC().Format(time.RFC3339Nano))
err = os.Rename(currentPath, revisionPath)
if err != nil {
t.log.Error().Err(err).Str("path", path).Str("revisionPath", revisionPath).Msg("could not create revision")
return
}
if err != nil {
t.log.Error().Err(err).Str("path", path).Str("currentPath", currentPath).Msg("could not stat current path")
return
}
revisionPath := t.lookup.VersionPath(n.SpaceID, n.ID, stat.ModTime().UTC().Format(time.RFC3339Nano))
// Copy the new version to the current version
if err := os.MkdirAll(filepath.Dir(currentPath), 0700); err != nil {
t.log.Error().Err(err).Str("path", path).Str("currentPath", currentPath).Msg("could not create base path for current file")
err = os.Rename(currentPath, revisionPath)
if err != nil {
t.log.Error().Err(err).Str("path", path).Str("revisionPath", revisionPath).Msg("could not create revision")
return
}
// Copy the new version to the current version
w, err := os.OpenFile(currentPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
t.log.Error().Err(err).Str("path", path).Str("currentPath", currentPath).Msg("could not open current path for writing")
return
}
defer w.Close()
r, err := os.OpenFile(path, os.O_RDONLY, 0600)
r, err := os.OpenFile(n.InternalPath(), os.O_RDONLY, 0600)
if err != nil {
t.log.Error().Err(err).Str("path", path).Msg("could not open file for reading")
return
@@ -768,7 +775,7 @@ assimilate:
}()
}
err = t.Propagate(context.Background(), n, sizeDiff)
err = t.Propagate(context.Background(), n, 0)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to propagate")
}

View File

@@ -8,7 +8,6 @@ import (
"encoding/json"
"path/filepath"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
kafka "github.com/segmentio/kafka-go"
@@ -98,17 +97,17 @@ func (w *CephFSWatcher) Watch(topic string) {
go func() {
switch {
case mask&CEPH_MDS_NOTIFY_DELETE > 0:
err = w.tree.Scan(path, watcher.ActionDelete, isDir)
err = w.tree.Scan(path, ActionDelete, isDir)
case mask&CEPH_MDS_NOTIFY_MOVED_TO > 0:
if ev.SrcMask > 0 {
// This is a move, clean up the old path
err = w.tree.Scan(filepath.Join(w.tree.options.WatchRoot, ev.SrcPath), watcher.ActionMoveFrom, isDir)
err = w.tree.Scan(filepath.Join(w.tree.options.WatchRoot, ev.SrcPath), ActionMoveFrom, isDir)
}
err = w.tree.Scan(path, watcher.ActionMove, isDir)
err = w.tree.Scan(path, ActionMove, isDir)
case mask&CEPH_MDS_NOTIFY_CREATE > 0:
err = w.tree.Scan(path, watcher.ActionCreate, isDir)
err = w.tree.Scan(path, ActionCreate, isDir)
case mask&CEPH_MDS_NOTIFY_CLOSE_WRITE > 0:
err = w.tree.Scan(path, watcher.ActionUpdate, isDir)
err = w.tree.Scan(path, ActionUpdate, isDir)
case mask&CEPH_MDS_NOTIFY_CLOSE > 0:
// ignore, already handled by CLOSE_WRITE
default:

View File

@@ -26,7 +26,6 @@ import (
"strconv"
"time"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
"github.com/rs/zerolog"
)
@@ -89,15 +88,15 @@ start:
go func() {
switch ev.Event {
case "CREATE":
err = w.tree.Scan(ev.Path, watcher.ActionCreate, false)
err = w.tree.Scan(ev.Path, ActionCreate, false)
case "CLOSE":
var bytesWritten int
bytesWritten, err = strconv.Atoi(ev.BytesWritten)
if err == nil && bytesWritten > 0 {
err = w.tree.Scan(ev.Path, watcher.ActionUpdate, false)
err = w.tree.Scan(ev.Path, ActionUpdate, false)
}
case "RENAME":
err = w.tree.Scan(ev.Path, watcher.ActionMove, false)
err = w.tree.Scan(ev.Path, ActionMove, false)
if warmupErr := w.tree.WarmupIDCache(ev.Path, false, false); warmupErr != nil {
w.log.Error().Err(warmupErr).Str("path", ev.Path).Msg("error warming up id cache")
}

View File

@@ -26,7 +26,6 @@ import (
"strconv"
"strings"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
"github.com/rs/zerolog"
kafka "github.com/segmentio/kafka-go"
)
@@ -78,21 +77,21 @@ func (w *GpfsWatchFolderWatcher) Watch(topic string) {
var err error
switch {
case strings.Contains(lwev.Event, "IN_DELETE"):
err = w.tree.Scan(path, watcher.ActionDelete, isDir)
err = w.tree.Scan(path, ActionDelete, isDir)
case strings.Contains(lwev.Event, "IN_MOVE_FROM"):
err = w.tree.Scan(path, watcher.ActionMoveFrom, isDir)
err = w.tree.Scan(path, ActionMoveFrom, isDir)
case strings.Contains(lwev.Event, "IN_CREATE"):
err = w.tree.Scan(path, watcher.ActionCreate, isDir)
err = w.tree.Scan(path, ActionCreate, isDir)
case strings.Contains(lwev.Event, "IN_CLOSE_WRITE"):
bytesWritten, convErr := strconv.Atoi(lwev.BytesWritten)
if convErr == nil && bytesWritten > 0 {
err = w.tree.Scan(path, watcher.ActionUpdate, isDir)
err = w.tree.Scan(path, ActionUpdate, isDir)
}
case strings.Contains(lwev.Event, "IN_MOVED_TO"):
err = w.tree.Scan(path, watcher.ActionMove, isDir)
err = w.tree.Scan(path, ActionMove, isDir)
}
if err != nil {
w.log.Error().Err(err).Str("path", path).Msg("error scanning path")

View File

@@ -30,7 +30,6 @@ import (
"time"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
"github.com/pablodz/inotifywaitgo/inotifywaitgo"
"github.com/rs/zerolog"
slogzerolog "github.com/samber/slog-zerolog/v2"
@@ -97,15 +96,15 @@ func (iw *InotifyWatcher) Watch(path string) {
var err error
switch e {
case inotifywaitgo.DELETE:
err = iw.tree.Scan(event.Filename, watcher.ActionDelete, event.IsDir)
err = iw.tree.Scan(event.Filename, ActionDelete, event.IsDir)
case inotifywaitgo.MOVED_FROM:
err = iw.tree.Scan(event.Filename, watcher.ActionMoveFrom, event.IsDir)
err = iw.tree.Scan(event.Filename, ActionMoveFrom, event.IsDir)
case inotifywaitgo.MOVED_TO:
err = iw.tree.Scan(event.Filename, watcher.ActionMove, event.IsDir)
err = iw.tree.Scan(event.Filename, ActionMove, event.IsDir)
case inotifywaitgo.CREATE:
err = iw.tree.Scan(event.Filename, watcher.ActionCreate, event.IsDir)
err = iw.tree.Scan(event.Filename, ActionCreate, event.IsDir)
case inotifywaitgo.CLOSE_WRITE:
err = iw.tree.Scan(event.Filename, watcher.ActionUpdate, event.IsDir)
err = iw.tree.Scan(event.Filename, ActionUpdate, event.IsDir)
case inotifywaitgo.CLOSE:
// ignore, already handled by CLOSE_WRITE
default:

View File

@@ -47,7 +47,6 @@ import (
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/lookup"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/trashbin"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher/natswatcher"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata"
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
@@ -148,11 +147,6 @@ func New(lu node.PathLookup, bs node.Blobstore, um usermapper.Mapper, trashbin *
if err != nil {
return nil, err
}
case "natswatcher":
t.watcher, err = natswatcher.New(context.TODO(), t, o.NatsWatcher, o.WatchRoot, log)
if err != nil {
return nil, err
}
default:
t.watcher, err = NewInotifyWatcher(t, o, log)
if err != nil {
@@ -505,18 +499,8 @@ func (t *Tree) ListFolder(ctx context.Context, n *node.Node) ([]*node.Node, erro
_, nodeID, err := t.lookup.IDsForPath(ctx, path)
if err != nil {
// we don't know about this node yet for some reason, assimilate it on the fly
t.log.Info().Err(err).Str("path", path).Msg("encountered unknown entity while listing the directory. Assimilate.")
err = t.assimilate(scanItem{Path: path})
if err != nil {
t.log.Error().Err(err).Str("path", path).Msg("failed to assimilate node")
continue
}
_, nodeID, err = t.lookup.IDsForPath(ctx, path)
if err != nil || nodeID == "" {
t.log.Error().Err(err).Str("path", path).Msg("still could not resolve node after assimilation")
continue
}
t.log.Error().Err(err).Str("path", path).Msg("failed to get ids for entry")
continue
}
child, err := node.ReadNode(ctx, t.lookup, n.SpaceID, nodeID, false, n.SpaceRoot, true)
@@ -724,23 +708,9 @@ func (t *Tree) createDirNode(ctx context.Context, n *node.Node) (err error) {
t.log.Error().Err(err).Str("spaceID", n.SpaceID).Str("id", n.ID).Str("path", path).Msg("could not cache id")
}
// Write mtime from filesystem to metadata to preven re-assimilation
d, err := os.Open(path)
if err != nil {
return err
}
fi, err := d.Stat()
if err != nil {
return err
}
mtime := fi.ModTime()
attributes := n.NodeMetadata(ctx)
attributes[prefixes.MTimeAttr] = []byte(mtime.UTC().Format(time.RFC3339Nano))
attributes[prefixes.IDAttr] = []byte(n.ID)
attributes[prefixes.TreesizeAttr] = []byte("0") // initialize as empty, TODO why bother? if it is not set we could treat it as 0?
if t.options.TreeTimeAccounting || t.options.TreeSizeAccounting {
attributes[prefixes.PropagationAttr] = []byte("1") // mark the node for propagation
}

View File

@@ -1,11 +0,0 @@
package watcher
type EventAction int
const (
ActionCreate EventAction = iota
ActionUpdate
ActionMove
ActionDelete
ActionMoveFrom
)

View File

@@ -1,236 +0,0 @@
package natswatcher
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"path/filepath"
"time"
"github.com/cenkalti/backoff"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher"
"github.com/rs/zerolog"
"github.com/vmihailenco/msgpack/v5"
)
// natsEvent represents the event encoded in MessagePack.
// we abbreviate the the properties to save some space
type natsEvent struct {
Event string `msgpack:"e"`
Path string `msgpack:"p,omitempty"`
ToPath string `msgpack:"t,omitempty"`
IsDir bool `msgpack:"d,omitempty"`
}
// NatsWatcher consumes filesystem-style events from NATS JetStream.
type NatsWatcher struct {
ctx context.Context
tree Scannable
log *zerolog.Logger
watchRoot string
config options.NatsWatcherConfig
}
type Scannable interface {
Scan(path string, action watcher.EventAction, isDir bool) error
}
// NewNatsWatcher creates a new NATS watcher.
func New(ctx context.Context, tree Scannable, cfg options.NatsWatcherConfig, watchRoot string, log *zerolog.Logger) (*NatsWatcher, error) {
return &NatsWatcher{
ctx: ctx,
tree: tree,
log: log,
watchRoot: watchRoot,
config: cfg,
}, nil
}
// Watch starts consuming events from a NATS JetStream subject
func (w *NatsWatcher) Watch(path string) {
w.log.Info().Str("stream", w.config.Stream).Msg("starting NATS watcher with auto-reconnect")
for {
select {
case <-w.ctx.Done():
w.log.Debug().Msg("context cancelled, stopping NATS watcher")
return
default:
}
// Try to connect with exponential backoff
nc, js, err := w.connectWithBackoff()
if err != nil {
w.log.Error().Err(err).Msg("failed to establish NATS connection after retries")
time.Sleep(5 * time.Second)
continue
}
if err := w.consume(js); err != nil {
w.log.Error().Err(err).Msg("NATS consumer exited with error, reconnecting")
}
_ = nc.Drain()
nc.Close()
time.Sleep(2 * time.Second)
}
}
// connectWithBackoff repeatedly attempts to connect to NATS JetStream with exponential backoff.
func (w *NatsWatcher) connectWithBackoff() (*nats.Conn, jetstream.JetStream, error) {
var nc *nats.Conn
var js jetstream.JetStream
b := backoff.NewExponentialBackOff()
b.InitialInterval = 1 * time.Second
b.MaxInterval = 30 * time.Second
b.MaxElapsedTime = 0 // never stop
connect := func() error {
select {
case <-w.ctx.Done():
return backoff.Permanent(w.ctx.Err())
default:
}
var err error
nc, err = w.connect()
if err != nil {
w.log.Warn().Err(err).Msg("failed to connect to NATS, retrying")
return err
}
js, err = jetstream.New(nc)
if err != nil {
nc.Close()
w.log.Warn().Err(err).Msg("failed to create jetstream context, retrying")
return err
}
w.log.Info().Str("endpoint", w.config.Endpoint).Msg("connected to NATS JetStream")
return nil
}
if err := backoff.Retry(connect, backoff.WithContext(b, w.ctx)); err != nil {
return nil, nil, err
}
return nc, js, nil
}
// consume subscribes to JetStream and handles messages.
func (w *NatsWatcher) consume(js jetstream.JetStream) error {
stream, err := js.Stream(w.ctx, w.config.Stream)
if err != nil {
return fmt.Errorf("failed to get stream: %w", err)
}
consumer, err := stream.CreateOrUpdateConsumer(w.ctx, jetstream.ConsumerConfig{
Durable: w.config.Durable,
AckPolicy: jetstream.AckExplicitPolicy,
MaxAckPending: w.config.MaxAckPending,
AckWait: w.config.AckWait,
})
if err != nil {
return fmt.Errorf("failed to create consumer: %w", err)
}
w.log.Info().
Str("stream", w.config.Stream).
Msg("started consuming from JetStream")
_, err = consumer.Consume(func(msg jetstream.Msg) {
defer func() {
if ackErr := msg.Ack(); ackErr != nil {
w.log.Warn().Err(ackErr).Msg("failed to ack message")
}
}()
var ev natsEvent
if err := msgpack.Unmarshal(msg.Data(), &ev); err != nil {
w.log.Error().Err(err).Msg("failed to decode MessagePack event")
return
}
w.handleEvent(ev)
})
if err != nil {
return fmt.Errorf("consumer error: %w", err)
}
<-w.ctx.Done()
return w.ctx.Err()
}
// connect establishes a single NATS connection with optional TLS and auth.
func (w *NatsWatcher) connect() (*nats.Conn, error) {
var tlsConf *tls.Config
if w.config.EnableTLS {
var rootCAPool *x509.CertPool
if w.config.TLSRootCACertificate != "" {
rootCrtFile, err := os.ReadFile(w.config.TLSRootCACertificate)
if err != nil {
return nil, fmt.Errorf("failed to read root CA: %w", err)
}
rootCAPool = x509.NewCertPool()
rootCAPool.AppendCertsFromPEM(rootCrtFile)
w.config.TLSInsecure = false
}
tlsConf = &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: w.config.TLSInsecure,
RootCAs: rootCAPool,
}
}
opts := []nats.Option{nats.Name("opencloud-posixfs-natswatcher")}
if tlsConf != nil {
opts = append(opts, nats.Secure(tlsConf))
}
if w.config.AuthUsername != "" && w.config.AuthPassword != "" {
opts = append(opts, nats.UserInfo(w.config.AuthUsername, w.config.AuthPassword))
}
return nats.Connect(w.config.Endpoint, opts...)
}
// handleEvent applies the event to the local tree.
func (w *NatsWatcher) handleEvent(ev natsEvent) {
var err error
// Determine the relevant path
path := filepath.Join(w.watchRoot, ev.Path)
switch ev.Event {
case "CREATE":
err = w.tree.Scan(path, watcher.ActionCreate, ev.IsDir)
case "MOVED_TO":
err = w.tree.Scan(path, watcher.ActionMove, ev.IsDir)
case "MOVE_FROM":
err = w.tree.Scan(path, watcher.ActionMoveFrom, ev.IsDir)
case "MOVE": // support event with source and target path
err = w.tree.Scan(path, watcher.ActionMoveFrom, ev.IsDir)
if err == nil {
w.log.Error().Err(err).Interface("event", ev).Msg("error processing event")
}
tgt := filepath.Join(w.watchRoot, ev.ToPath)
if tgt == "" {
w.log.Warn().Interface("event", ev).Msg("MOVE event missing target path")
} else {
err = w.tree.Scan(tgt, watcher.ActionMove, ev.IsDir)
}
case "CLOSE_WRITE":
err = w.tree.Scan(path, watcher.ActionUpdate, ev.IsDir)
case "DELETE":
err = w.tree.Scan(path, watcher.ActionDelete, ev.IsDir)
default:
w.log.Warn().Str("event", ev.Event).Msg("unhandled event type")
}
if err != nil {
w.log.Error().Err(err).Interface("event", ev).Msg("error processing event")
}
}

View File

@@ -2,7 +2,6 @@ package metadata
import (
"context"
"fmt"
"io"
"io/fs"
"os"
@@ -293,19 +292,17 @@ func (b HybridBackend) SetMultiple(ctx context.Context, n MetadataNode, attribs
}
}
xerrs := 0
total := 0
var xerr error
// error handling: Count if there are errors while setting the attribs.
// if there were any, return an error.
for key, val := range attribs {
total++
if xerr = xattr.Set(path, key, val); xerr != nil {
// log
xerrs++
}
}
if xerrs > 0 {
return fmt.Errorf("failed to set %d/%d xattrs: %w", xerrs, total, xerr)
return errors.Wrap(xerr, "Failed to set all xattrs")
}
attribs, err = b.getAll(ctx, n, true, false, false)

4
vendor/modules.txt vendored
View File

@@ -1368,7 +1368,7 @@ github.com/opencloud-eu/icap-client
# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20250724122329-41ba6b191e76
## explicit; go 1.18
github.com/opencloud-eu/libre-graph-api-go
# github.com/opencloud-eu/reva/v2 v2.41.0
# github.com/opencloud-eu/reva/v2 v2.40.1
## explicit; go 1.24.1
github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace
github.com/opencloud-eu/reva/v2/cmd/revad/runtime
@@ -1682,8 +1682,6 @@ github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options
github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/timemanager
github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/trashbin
github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/tree
github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher
github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/watcher/natswatcher
github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry
github.com/opencloud-eu/reva/v2/pkg/storage/fs/s3ng
github.com/opencloud-eu/reva/v2/pkg/storage/fs/s3ng/blobstore