From 57efd44b0a88d6fc950f459a72df31fb6df519a7 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Wed, 22 Apr 2026 13:41:22 +0200 Subject: [PATCH 1/2] feat(web): introduce dynamic theme management via RPC adds support for adding, removing, and checking themes at runtime. replaces static themes with a shared storage-based (cs3 system-storage) solution suitable for distributed systems. --- pkg/x/io/fsx/cs3/metadata/file.go | 89 ++++ pkg/x/io/fsx/cs3/metadata/file_info.go | 59 +++ pkg/x/io/fsx/cs3/metadata/fs.go | 96 ++++ pkg/x/io/fsx/cs3/metadata/metadata.go | 9 + .../gen/opencloud/services/web/v0/web.pb.go | 499 ++++++++++++++++++ .../opencloud/services/web/v0/web.pb.micro.go | 122 +++++ .../services/web/v0/web.swagger.json | 80 +++ protogen/proto/buf.gen.yaml | 1 + .../proto/opencloud/services/web/v0/web.proto | 58 ++ services/web/Makefile | 7 + services/web/pkg/command/server.go | 23 + services/web/pkg/config/config.go | 22 +- .../web/pkg/config/defaults/defaultconfig.go | 28 +- services/web/pkg/config/grpc.go | 11 + services/web/pkg/fs/fs.go | 52 ++ services/web/pkg/server/grpc/option.go | 94 ++++ services/web/pkg/server/grpc/server.go | 67 +++ services/web/pkg/server/http/server.go | 14 +- services/web/pkg/service/grpc/v0/option.go | 68 +++ services/web/pkg/service/grpc/v0/service.go | 67 +++ services/web/pkg/service/v0/service.go | 89 ++-- services/web/pkg/theme/http.go | 72 +++ services/web/pkg/theme/http_test.go | 57 ++ services/web/pkg/theme/kv.go | 64 +-- services/web/pkg/theme/kv_test.go | 137 +---- services/web/pkg/theme/service.go | 186 +++---- services/web/pkg/theme/service_test.go | 51 +- services/web/pkg/theme/theme.go | 6 +- 28 files changed, 1700 insertions(+), 428 deletions(-) create mode 100644 pkg/x/io/fsx/cs3/metadata/file.go create mode 100644 pkg/x/io/fsx/cs3/metadata/file_info.go create mode 100644 pkg/x/io/fsx/cs3/metadata/fs.go create mode 100644 pkg/x/io/fsx/cs3/metadata/metadata.go create mode 100644 protogen/gen/opencloud/services/web/v0/web.pb.go create mode 100644 protogen/gen/opencloud/services/web/v0/web.pb.micro.go create mode 100644 protogen/gen/opencloud/services/web/v0/web.swagger.json create mode 100644 protogen/proto/opencloud/services/web/v0/web.proto create mode 100644 services/web/pkg/config/grpc.go create mode 100644 services/web/pkg/fs/fs.go create mode 100644 services/web/pkg/server/grpc/option.go create mode 100644 services/web/pkg/server/grpc/server.go create mode 100644 services/web/pkg/service/grpc/v0/option.go create mode 100644 services/web/pkg/service/grpc/v0/service.go create mode 100644 services/web/pkg/theme/http.go create mode 100644 services/web/pkg/theme/http_test.go diff --git a/pkg/x/io/fsx/cs3/metadata/file.go b/pkg/x/io/fsx/cs3/metadata/file.go new file mode 100644 index 0000000000..4980058e7b --- /dev/null +++ b/pkg/x/io/fsx/cs3/metadata/file.go @@ -0,0 +1,89 @@ +package metadata + +import ( + "bytes" + "context" + "io" + "io/fs" + "os" +) + +type File struct { + name string + fs *Fs + fileMode os.FileMode + content []byte + resource io.ReadCloser +} + +func newFile(name string, fs *Fs, fileMode os.FileMode, content []byte) (*File, error) { + return &File{ + name: name, + fs: fs, + fileMode: fileMode, + content: content, + resource: io.NopCloser(bytes.NewBuffer(content)), + }, nil +} + +func (f *File) Close() error { + return f.resource.Close() +} + +func (f *File) Read(p []byte) (n int, err error) { + return f.resource.Read(p) +} + +func (f *File) ReadAt(p []byte, off int64) (n int, err error) { + readerAt, ok := f.resource.(io.ReaderAt) + if !ok { + return -1, &fs.PathError{Op: "ReadAt", Path: f.name, Err: ErrNotImplemented} + } + + return readerAt.ReadAt(p, off) +} + +func (f *File) Seek(offset int64, whence int) (int64, error) { + seeker, ok := f.resource.(io.Seeker) + if !ok { + return -1, &fs.PathError{Op: "Seek", Path: f.name, Err: ErrNotImplemented} + } + + return seeker.Seek(offset, whence) +} + +func (f *File) Write(p []byte) (n int, err error) { + return len(p), f.fs.storage.SimpleUpload(context.Background(), f.name, p) +} + +func (f *File) WriteAt(_ []byte, _ int64) (n int, err error) { + return -1, &fs.PathError{Op: "Write", Path: f.name, Err: ErrNotImplemented} +} + +func (f *File) Name() string { + return f.name +} + +func (f *File) Readdir(_ int) ([]os.FileInfo, error) { + return nil, &fs.PathError{Op: "Readdir", Path: f.name, Err: ErrNotImplemented} +} + +func (f *File) Readdirnames(_ int) ([]string, error) { + return nil, &fs.PathError{Op: "Readdirnames", Path: f.name, Err: ErrNotImplemented} +} + +func (f *File) Sync() error { + return nil +} + +func (f *File) Truncate(_ int64) error { + return &fs.PathError{Op: "Truncate", Path: f.name, Err: ErrNotImplemented} +} + +func (f *File) WriteString(_ string) (ret int, err error) { + return -1, &fs.PathError{Op: "WriteString", Path: f.name, Err: ErrNotImplemented} +} + +func (f *File) Stat() (os.FileInfo, error) { + return newFileInfo(f.name, f.fs, f.fileMode) +} diff --git a/pkg/x/io/fsx/cs3/metadata/file_info.go b/pkg/x/io/fsx/cs3/metadata/file_info.go new file mode 100644 index 0000000000..562ca4c193 --- /dev/null +++ b/pkg/x/io/fsx/cs3/metadata/file_info.go @@ -0,0 +1,59 @@ +package metadata + +import ( + "context" + "io/fs" + "os" + "time" + + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + + "github.com/opencloud-eu/reva/v2/pkg/utils" +) + +type FileInfo struct { + name string + size int64 + modTime time.Time + isDir bool + mode os.FileMode +} + +func newFileInfo(name string, fs *Fs, fileMode os.FileMode) (*FileInfo, error) { + info, err := fs.storage.Stat(context.Background(), name) + if err != nil { + return nil, err + } + + return &FileInfo{ + name: info.GetName(), + size: int64(info.GetSize()), + modTime: utils.TSToTime(info.GetMtime()), + isDir: info.GetType() == providerv1beta1.ResourceType_RESOURCE_TYPE_CONTAINER, + mode: fileMode, + }, nil +} + +func (f *FileInfo) Name() string { + return f.name +} + +func (f *FileInfo) Size() int64 { + return f.size +} + +func (f *FileInfo) ModTime() time.Time { + return f.modTime +} + +func (f *FileInfo) IsDir() bool { + return f.isDir +} + +func (f *FileInfo) Mode() fs.FileMode { + return f.mode +} + +func (f *FileInfo) Sys() any { + return nil +} diff --git a/pkg/x/io/fsx/cs3/metadata/fs.go b/pkg/x/io/fsx/cs3/metadata/fs.go new file mode 100644 index 0000000000..c62337a1c3 --- /dev/null +++ b/pkg/x/io/fsx/cs3/metadata/fs.go @@ -0,0 +1,96 @@ +package metadata + +import ( + "context" + "fmt" + "io/fs" + "os" + "path" + "strings" + "syscall" + "time" + + "github.com/spf13/afero" + + revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata" + "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" +) + +func NewMetadataFs(storage metadata.Storage) *Fs { + return &Fs{storage: storage} +} + +type Fs struct { + storage metadata.Storage +} + +func (fs *Fs) Create(_ string) (afero.File, error) { + return nil, syscall.EPERM +} + +func (fs *Fs) Mkdir(name string, _ os.FileMode) error { + return fs.storage.MakeDirIfNotExist(context.Background(), name) +} + +func (fs *Fs) MkdirAll(name string, _ os.FileMode) error { + paths := strings.Split(name, string(os.PathSeparator)) + // Create all parent directories if they do not exist + for i := 0; i <= len(paths)-1; i++ { + c := path.Join(paths[:i+1]...) + if err := fs.storage.MakeDirIfNotExist(context.Background(), c); err != nil { + return fmt.Errorf("failed to create directory %s: %w", c, err) + } + } + + return nil +} + +func (fs *Fs) Open(name string) (afero.File, error) { + return fs.OpenFile(name, os.O_RDONLY, 0) +} + +func (fs *Fs) OpenFile(name string, _ int, _ os.FileMode) (afero.File, error) { + res, err := fs.storage.Download(context.Background(), metadata.DownloadRequest{Path: name}) + if err != nil && !revaMetadata.IsNotExist(err) { + return nil, err + } + + var contend []byte + if res != nil { + contend = res.Content + } + + return newFile(name, fs, 0, contend) +} + +func (fs *Fs) Remove(name string) error { + return fs.RemoveAll(name) +} + +func (fs *Fs) RemoveAll(path string) error { + return fs.storage.Delete(context.Background(), path) +} + +func (fs *Fs) Rename(_, _ string) error { + return syscall.EPERM +} + +func (fs *Fs) Stat(name string) (fs.FileInfo, error) { + return newFileInfo(name, fs, 0) +} + +func (fs *Fs) Name() string { + return "MetadataFS" +} + +func (fs *Fs) Chmod(_ string, _ os.FileMode) error { + return syscall.EPERM +} + +func (fs *Fs) Chown(_ string, _, _ int) error { + return syscall.EPERM +} + +func (fs *Fs) Chtimes(_ string, _ time.Time, _ time.Time) error { + return syscall.EPERM +} diff --git a/pkg/x/io/fsx/cs3/metadata/metadata.go b/pkg/x/io/fsx/cs3/metadata/metadata.go new file mode 100644 index 0000000000..aecf62fdaf --- /dev/null +++ b/pkg/x/io/fsx/cs3/metadata/metadata.go @@ -0,0 +1,9 @@ +package metadata + +import ( + "errors" +) + +var ( + ErrNotImplemented = errors.New("not implemented") +) diff --git a/protogen/gen/opencloud/services/web/v0/web.pb.go b/protogen/gen/opencloud/services/web/v0/web.pb.go new file mode 100644 index 0000000000..badb1022d2 --- /dev/null +++ b/protogen/gen/opencloud/services/web/v0/web.pb.go @@ -0,0 +1,499 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: opencloud/services/web/v0/web.proto + +package v0 + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ThemeAddRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // The ID of the theme to add + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` // The theme data in bytes +} + +func (x *ThemeAddRequest) Reset() { + *x = ThemeAddRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ThemeAddRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThemeAddRequest) ProtoMessage() {} + +func (x *ThemeAddRequest) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThemeAddRequest.ProtoReflect.Descriptor instead. +func (*ThemeAddRequest) Descriptor() ([]byte, []int) { + return file_opencloud_services_web_v0_web_proto_rawDescGZIP(), []int{0} +} + +func (x *ThemeAddRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ThemeAddRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type ThemeAddResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ThemeAddResponse) Reset() { + *x = ThemeAddResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ThemeAddResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThemeAddResponse) ProtoMessage() {} + +func (x *ThemeAddResponse) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThemeAddResponse.ProtoReflect.Descriptor instead. +func (*ThemeAddResponse) Descriptor() ([]byte, []int) { + return file_opencloud_services_web_v0_web_proto_rawDescGZIP(), []int{1} +} + +type ThemeExistsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // The ID of the theme to check +} + +func (x *ThemeExistsRequest) Reset() { + *x = ThemeExistsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ThemeExistsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThemeExistsRequest) ProtoMessage() {} + +func (x *ThemeExistsRequest) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThemeExistsRequest.ProtoReflect.Descriptor instead. +func (*ThemeExistsRequest) Descriptor() ([]byte, []int) { + return file_opencloud_services_web_v0_web_proto_rawDescGZIP(), []int{2} +} + +func (x *ThemeExistsRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type ThemeExistsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` // Indicates if the theme exists +} + +func (x *ThemeExistsResponse) Reset() { + *x = ThemeExistsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ThemeExistsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThemeExistsResponse) ProtoMessage() {} + +func (x *ThemeExistsResponse) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThemeExistsResponse.ProtoReflect.Descriptor instead. +func (*ThemeExistsResponse) Descriptor() ([]byte, []int) { + return file_opencloud_services_web_v0_web_proto_rawDescGZIP(), []int{3} +} + +func (x *ThemeExistsResponse) GetExists() bool { + if x != nil { + return x.Exists + } + return false +} + +type ThemeRemoveRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // The ID of the theme to remove +} + +func (x *ThemeRemoveRequest) Reset() { + *x = ThemeRemoveRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ThemeRemoveRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThemeRemoveRequest) ProtoMessage() {} + +func (x *ThemeRemoveRequest) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThemeRemoveRequest.ProtoReflect.Descriptor instead. +func (*ThemeRemoveRequest) Descriptor() ([]byte, []int) { + return file_opencloud_services_web_v0_web_proto_rawDescGZIP(), []int{4} +} + +func (x *ThemeRemoveRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type ThemeRemoveResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ThemeRemoveResponse) Reset() { + *x = ThemeRemoveResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ThemeRemoveResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ThemeRemoveResponse) ProtoMessage() {} + +func (x *ThemeRemoveResponse) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_web_v0_web_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ThemeRemoveResponse.ProtoReflect.Descriptor instead. +func (*ThemeRemoveResponse) Descriptor() ([]byte, []int) { + return file_opencloud_services_web_v0_web_proto_rawDescGZIP(), []int{5} +} + +var File_opencloud_services_web_v0_web_proto protoreflect.FileDescriptor + +var file_opencloud_services_web_v0_web_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2f, 0x77, 0x65, 0x62, 0x2f, 0x76, 0x30, 0x2f, 0x77, 0x65, 0x62, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x77, 0x65, 0x62, 0x2e, 0x76, 0x30, + 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, + 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, + 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x35, 0x0a, 0x0f, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x12, 0x0a, 0x10, 0x54, 0x68, 0x65, 0x6d, 0x65, + 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x24, 0x0a, 0x12, 0x54, + 0x68, 0x65, 0x6d, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x22, 0x2d, 0x0a, 0x13, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, + 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, + 0x22, 0x24, 0x0a, 0x12, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xcd, 0x02, + 0x0a, 0x0a, 0x57, 0x65, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x63, 0x0a, 0x08, + 0x54, 0x68, 0x65, 0x6d, 0x65, 0x41, 0x64, 0x64, 0x12, 0x2a, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, + 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x77, 0x65, + 0x62, 0x2e, 0x76, 0x30, 0x2e, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x41, 0x64, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x77, 0x65, 0x62, 0x2e, 0x76, 0x30, + 0x2e, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x41, 0x64, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x6c, 0x0a, 0x0b, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, + 0x12, 0x2d, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x77, 0x65, 0x62, 0x2e, 0x76, 0x30, 0x2e, 0x54, 0x68, 0x65, + 0x6d, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2e, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x77, 0x65, 0x62, 0x2e, 0x76, 0x30, 0x2e, 0x54, 0x68, 0x65, 0x6d, + 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x6c, 0x0a, 0x0b, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x12, 0x2d, + 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x77, 0x65, 0x62, 0x2e, 0x76, 0x30, 0x2e, 0x54, 0x68, 0x65, 0x6d, 0x65, + 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, + 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x77, 0x65, 0x62, 0x2e, 0x76, 0x30, 0x2e, 0x54, 0x68, 0x65, 0x6d, 0x65, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xea, 0x02, + 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, + 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, + 0x6f, 0x75, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, + 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2f, 0x77, 0x65, 0x62, 0x2f, 0x76, 0x30, 0x92, 0x41, 0x9c, 0x02, 0x12, 0xb4, + 0x01, 0x0a, 0x0d, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x77, 0x65, 0x62, + 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, + 0x62, 0x48, 0x12, 0x29, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x1a, 0x14, 0x73, + 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x2e, 0x65, 0x75, 0x2a, 0x49, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, + 0x30, 0x12, 0x3b, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, + 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x62, 0x6c, 0x6f, + 0x62, 0x2f, 0x6d, 0x61, 0x69, 0x6e, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, + 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72, 0x3b, 0x0a, + 0x10, 0x44, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, + 0x6c, 0x12, 0x27, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2e, + 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x65, 0x75, 0x2f, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x77, 0x65, 0x62, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_opencloud_services_web_v0_web_proto_rawDescOnce sync.Once + file_opencloud_services_web_v0_web_proto_rawDescData = file_opencloud_services_web_v0_web_proto_rawDesc +) + +func file_opencloud_services_web_v0_web_proto_rawDescGZIP() []byte { + file_opencloud_services_web_v0_web_proto_rawDescOnce.Do(func() { + file_opencloud_services_web_v0_web_proto_rawDescData = protoimpl.X.CompressGZIP(file_opencloud_services_web_v0_web_proto_rawDescData) + }) + return file_opencloud_services_web_v0_web_proto_rawDescData +} + +var file_opencloud_services_web_v0_web_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_opencloud_services_web_v0_web_proto_goTypes = []interface{}{ + (*ThemeAddRequest)(nil), // 0: opencloud.services.web.v0.ThemeAddRequest + (*ThemeAddResponse)(nil), // 1: opencloud.services.web.v0.ThemeAddResponse + (*ThemeExistsRequest)(nil), // 2: opencloud.services.web.v0.ThemeExistsRequest + (*ThemeExistsResponse)(nil), // 3: opencloud.services.web.v0.ThemeExistsResponse + (*ThemeRemoveRequest)(nil), // 4: opencloud.services.web.v0.ThemeRemoveRequest + (*ThemeRemoveResponse)(nil), // 5: opencloud.services.web.v0.ThemeRemoveResponse +} +var file_opencloud_services_web_v0_web_proto_depIdxs = []int32{ + 0, // 0: opencloud.services.web.v0.WebService.ThemeAdd:input_type -> opencloud.services.web.v0.ThemeAddRequest + 2, // 1: opencloud.services.web.v0.WebService.ThemeExists:input_type -> opencloud.services.web.v0.ThemeExistsRequest + 4, // 2: opencloud.services.web.v0.WebService.ThemeRemove:input_type -> opencloud.services.web.v0.ThemeRemoveRequest + 1, // 3: opencloud.services.web.v0.WebService.ThemeAdd:output_type -> opencloud.services.web.v0.ThemeAddResponse + 3, // 4: opencloud.services.web.v0.WebService.ThemeExists:output_type -> opencloud.services.web.v0.ThemeExistsResponse + 5, // 5: opencloud.services.web.v0.WebService.ThemeRemove:output_type -> opencloud.services.web.v0.ThemeRemoveResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_opencloud_services_web_v0_web_proto_init() } +func file_opencloud_services_web_v0_web_proto_init() { + if File_opencloud_services_web_v0_web_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_opencloud_services_web_v0_web_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ThemeAddRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_opencloud_services_web_v0_web_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ThemeAddResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_opencloud_services_web_v0_web_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ThemeExistsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_opencloud_services_web_v0_web_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ThemeExistsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_opencloud_services_web_v0_web_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ThemeRemoveRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_opencloud_services_web_v0_web_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ThemeRemoveResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_opencloud_services_web_v0_web_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_opencloud_services_web_v0_web_proto_goTypes, + DependencyIndexes: file_opencloud_services_web_v0_web_proto_depIdxs, + MessageInfos: file_opencloud_services_web_v0_web_proto_msgTypes, + }.Build() + File_opencloud_services_web_v0_web_proto = out.File + file_opencloud_services_web_v0_web_proto_rawDesc = nil + file_opencloud_services_web_v0_web_proto_goTypes = nil + file_opencloud_services_web_v0_web_proto_depIdxs = nil +} diff --git a/protogen/gen/opencloud/services/web/v0/web.pb.micro.go b/protogen/gen/opencloud/services/web/v0/web.pb.micro.go new file mode 100644 index 0000000000..4e351b033e --- /dev/null +++ b/protogen/gen/opencloud/services/web/v0/web.pb.micro.go @@ -0,0 +1,122 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: opencloud/services/web/v0/web.proto + +package v0 + +import ( + fmt "fmt" + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + proto "google.golang.org/protobuf/proto" + math "math" +) + +import ( + context "context" + api "go-micro.dev/v4/api" + client "go-micro.dev/v4/client" + server "go-micro.dev/v4/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// Reference imports to suppress errors if they are not otherwise used. +var _ api.Endpoint +var _ context.Context +var _ client.Option +var _ server.Option + +// Api Endpoints for WebService service + +func NewWebServiceEndpoints() []*api.Endpoint { + return []*api.Endpoint{} +} + +// Client API for WebService service + +type WebService interface { + ThemeAdd(ctx context.Context, in *ThemeAddRequest, opts ...client.CallOption) (*ThemeAddResponse, error) + ThemeExists(ctx context.Context, in *ThemeExistsRequest, opts ...client.CallOption) (*ThemeExistsResponse, error) + ThemeRemove(ctx context.Context, in *ThemeRemoveRequest, opts ...client.CallOption) (*ThemeRemoveResponse, error) +} + +type webService struct { + c client.Client + name string +} + +func NewWebService(name string, c client.Client) WebService { + return &webService{ + c: c, + name: name, + } +} + +func (c *webService) ThemeAdd(ctx context.Context, in *ThemeAddRequest, opts ...client.CallOption) (*ThemeAddResponse, error) { + req := c.c.NewRequest(c.name, "WebService.ThemeAdd", in) + out := new(ThemeAddResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *webService) ThemeExists(ctx context.Context, in *ThemeExistsRequest, opts ...client.CallOption) (*ThemeExistsResponse, error) { + req := c.c.NewRequest(c.name, "WebService.ThemeExists", in) + out := new(ThemeExistsResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *webService) ThemeRemove(ctx context.Context, in *ThemeRemoveRequest, opts ...client.CallOption) (*ThemeRemoveResponse, error) { + req := c.c.NewRequest(c.name, "WebService.ThemeRemove", in) + out := new(ThemeRemoveResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for WebService service + +type WebServiceHandler interface { + ThemeAdd(context.Context, *ThemeAddRequest, *ThemeAddResponse) error + ThemeExists(context.Context, *ThemeExistsRequest, *ThemeExistsResponse) error + ThemeRemove(context.Context, *ThemeRemoveRequest, *ThemeRemoveResponse) error +} + +func RegisterWebServiceHandler(s server.Server, hdlr WebServiceHandler, opts ...server.HandlerOption) error { + type webService interface { + ThemeAdd(ctx context.Context, in *ThemeAddRequest, out *ThemeAddResponse) error + ThemeExists(ctx context.Context, in *ThemeExistsRequest, out *ThemeExistsResponse) error + ThemeRemove(ctx context.Context, in *ThemeRemoveRequest, out *ThemeRemoveResponse) error + } + type WebService struct { + webService + } + h := &webServiceHandler{hdlr} + return s.Handle(s.NewHandler(&WebService{h}, opts...)) +} + +type webServiceHandler struct { + WebServiceHandler +} + +func (h *webServiceHandler) ThemeAdd(ctx context.Context, in *ThemeAddRequest, out *ThemeAddResponse) error { + return h.WebServiceHandler.ThemeAdd(ctx, in, out) +} + +func (h *webServiceHandler) ThemeExists(ctx context.Context, in *ThemeExistsRequest, out *ThemeExistsResponse) error { + return h.WebServiceHandler.ThemeExists(ctx, in, out) +} + +func (h *webServiceHandler) ThemeRemove(ctx context.Context, in *ThemeRemoveRequest, out *ThemeRemoveResponse) error { + return h.WebServiceHandler.ThemeRemove(ctx, in, out) +} diff --git a/protogen/gen/opencloud/services/web/v0/web.swagger.json b/protogen/gen/opencloud/services/web/v0/web.swagger.json new file mode 100644 index 0000000000..2e48770b69 --- /dev/null +++ b/protogen/gen/opencloud/services/web/v0/web.swagger.json @@ -0,0 +1,80 @@ +{ + "swagger": "2.0", + "info": { + "title": "OpenCloud web", + "version": "1.0.0", + "contact": { + "name": "OpenCloud GmbH", + "url": "https://github.com/opencloud-eu/opencloud", + "email": "support@opencloud.eu" + }, + "license": { + "name": "Apache-2.0", + "url": "https://github.com/opencloud-eu/opencloud/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "WebService" + } + ], + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": {}, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v0ThemeAddResponse": { + "type": "object" + }, + "v0ThemeExistsResponse": { + "type": "object", + "properties": { + "exists": { + "type": "boolean", + "title": "Indicates if the theme exists" + } + } + }, + "v0ThemeRemoveResponse": { + "type": "object" + } + }, + "externalDocs": { + "description": "Developer Manual", + "url": "https://docs.opencloud.eu/services/web/" + } +} diff --git a/protogen/proto/buf.gen.yaml b/protogen/proto/buf.gen.yaml index 1e7689dce4..adca608319 100644 --- a/protogen/proto/buf.gen.yaml +++ b/protogen/proto/buf.gen.yaml @@ -25,6 +25,7 @@ plugins: opencloud.services.eventhistory.v0;\ opencloud.messages.eventhistory.v0;\ opencloud.services.policies.v0;\ + opencloud.services.web.v0;\ opencloud.messages.policies.v0" - name: openapiv2 diff --git a/protogen/proto/opencloud/services/web/v0/web.proto b/protogen/proto/opencloud/services/web/v0/web.proto new file mode 100644 index 0000000000..46da4b2a57 --- /dev/null +++ b/protogen/proto/opencloud/services/web/v0/web.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package opencloud.services.web.v0; + +option go_package = "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/web/v0"; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "OpenCloud web"; + version: "1.0.0"; + contact: { + name: "OpenCloud GmbH"; + url: "https://github.com/opencloud-eu/opencloud"; + email: "support@opencloud.eu"; + }; + license: { + name: "Apache-2.0"; + url: "https://github.com/opencloud-eu/opencloud/blob/main/LICENSE"; + }; + }; + schemes: HTTP; + schemes: HTTPS; + consumes: "application/json"; + produces: "application/json"; + external_docs: { + description: "Developer Manual"; + url: "https://docs.opencloud.eu/services/web/"; + }; +}; + +service WebService { + rpc ThemeAdd(ThemeAddRequest) returns (ThemeAddResponse); + rpc ThemeExists(ThemeExistsRequest) returns (ThemeExistsResponse); + rpc ThemeRemove(ThemeRemoveRequest) returns (ThemeRemoveResponse); +} + +message ThemeAddRequest { + string id = 1; // The ID of the theme to add + bytes data = 2; // The theme data in bytes +} + +message ThemeAddResponse {} + +message ThemeExistsRequest { + string id = 1; // The ID of the theme to check +} + +message ThemeExistsResponse { + bool exists = 1; // Indicates if the theme exists +} + +message ThemeRemoveRequest { + string id = 1; // The ID of the theme to remove +} + +message ThemeRemoveResponse {} diff --git a/services/web/Makefile b/services/web/Makefile index 5c172a5e70..0ecba31b45 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -11,6 +11,13 @@ include ../../.make/default.mk include ../../.make/go.mk include ../../.make/release.mk include ../../.make/docs.mk +include ../../.make/protobuf.mk + +.PHONY: go-generate +go-generate: protobuf + +.PHONY: protobuf +protobuf: buf-generate .PHONY: node-generate-dev node-generate-dev: pull-assets diff --git a/services/web/pkg/command/server.go b/services/web/pkg/command/server.go index 38df9caec4..145decf00d 100644 --- a/services/web/pkg/command/server.go +++ b/services/web/pkg/command/server.go @@ -10,11 +10,13 @@ import ( "github.com/opencloud-eu/opencloud/pkg/config/configlog" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/runner" + ogrpc "github.com/opencloud-eu/opencloud/pkg/service/grpc" "github.com/opencloud-eu/opencloud/pkg/tracing" "github.com/opencloud-eu/opencloud/services/web/pkg/config" "github.com/opencloud-eu/opencloud/services/web/pkg/config/parser" "github.com/opencloud-eu/opencloud/services/web/pkg/metrics" "github.com/opencloud-eu/opencloud/services/web/pkg/server/debug" + "github.com/opencloud-eu/opencloud/services/web/pkg/server/grpc" "github.com/opencloud-eu/opencloud/services/web/pkg/server/http" "github.com/spf13/cobra" @@ -48,6 +50,10 @@ func Server(cfg *config.Config) *cobra.Command { } } + cfg.GrpcClient, err = ogrpc.NewClient( + append(ogrpc.GetClientOptions(cfg.GRPCClientTLS), ogrpc.WithTraceProvider(traceProvider))..., + ) + var cancel context.CancelFunc if cfg.Context == nil { cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...) @@ -79,6 +85,23 @@ func Server(cfg *config.Config) *cobra.Command { gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", server)) } + { + grpcServer, err := grpc.Server( + grpc.Config(cfg), + grpc.Logger(logger), + grpc.Name(cfg.Service.Name), + grpc.Context(ctx), + grpc.JWTSecret(cfg.TokenManager.JWTSecret), + grpc.TraceProvider(traceProvider), + ) + if err != nil { + logger.Info().Err(err).Str("transport", "grpc").Msg("Failed to initialize server") + return err + } + + gr.Add(runner.NewGoMicroGrpcServerRunner(cfg.Service.Name+".grpc", grpcServer)) + } + { debugServer, err := debug.Server( debug.Logger(logger), diff --git a/services/web/pkg/config/config.go b/services/web/pkg/config/config.go index e2edcab53a..ac391ded42 100644 --- a/services/web/pkg/config/config.go +++ b/services/web/pkg/config/config.go @@ -3,6 +3,8 @@ package config import ( "context" + "go-micro.dev/v4/client" + "github.com/opencloud-eu/opencloud/pkg/shared" ) @@ -17,6 +19,9 @@ type Config struct { HTTP HTTP `yaml:"http"` + GRPC GRPCConfig `yaml:"grpc"` + GrpcClient client.Client `yaml:"-"` + Asset Asset `yaml:"asset"` File string `yaml:"file" env:"WEB_UI_CONFIG_FILE" desc:"Read the OpenCloud Web json based configuration from this path/file. The config file takes precedence over WEB_OPTION_xxx environment variables. See the text description for more details." introductionVersion:"1.0.0"` Web Web `yaml:"web"` @@ -24,8 +29,21 @@ type Config struct { TokenManager *TokenManager `yaml:"token_manager"` - GatewayAddress string `yaml:"gateway_addr" env:"WEB_GATEWAY_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"1.0.0"` - Context context.Context `yaml:"-"` + GatewayAddress string `yaml:"gateway_addr" env:"WEB_GATEWAY_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"1.0.0"` + Context context.Context `yaml:"-"` + GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` + + Metadata Metadata `yaml:"metadata_config"` +} + +// Metadata configures the metadata store to use +type Metadata struct { + GatewayAddress string `yaml:"gateway_addr" env:"WEB_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"%%NEXT%%"` + StorageAddress string `yaml:"storage_addr" env:"WEB_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"%%NEXT%%"` + + SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;WEB_SYSTEM_USER_ID" desc:"ID of the OpenCloud STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"%%NEXT%%"` + SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;WEB_SYSTEM_USER_IDP" desc:"IDP of the OpenCloud STORAGE-SYSTEM system user." introductionVersion:"%%NEXT%%"` + SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"%%NEXT%%"` } // Asset defines the available asset configuration. diff --git a/services/web/pkg/config/defaults/defaultconfig.go b/services/web/pkg/config/defaults/defaultconfig.go index 8120394aa6..98514dce79 100644 --- a/services/web/pkg/config/defaults/defaultconfig.go +++ b/services/web/pkg/config/defaults/defaultconfig.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/opencloud-eu/opencloud/pkg/config/defaults" + "github.com/opencloud-eu/opencloud/pkg/structs" "github.com/opencloud-eu/opencloud/services/web/pkg/config" ) @@ -25,6 +26,11 @@ func DefaultConfig() *config.Config { Pprof: false, Zpages: false, }, + GRPC: config.GRPCConfig{ + Addr: "127.0.0.1:9221", + Namespace: "eu.opencloud.api", + Protocol: "tcp", + }, HTTP: config.HTTP{ Addr: "127.0.0.1:9100", Root: "/", @@ -85,12 +91,16 @@ func DefaultConfig() *config.Config { ThemesPath: filepath.Join(defaults.BaseDataPath(), "web/assets/themes"), }, GatewayAddress: "eu.opencloud.api.gateway", + Metadata: config.Metadata{ + GatewayAddress: "eu.opencloud.api.storage-system", + StorageAddress: "eu.opencloud.api.storage-system", + SystemUserIDP: "internal", + }, Web: config.Web{ ThemeServer: "https://localhost:9200", ThemePath: "/themes/opencloud/theme.json", Config: config.WebConfig{ Server: "https://localhost:9200", - Theme: "", OpenIDConnect: config.OIDC{ MetadataURL: "", Authority: "https://localhost:9200", @@ -140,6 +150,14 @@ func EnsureDefaults(cfg *config.Config) { cfg.HTTP.CORS.AllowedOrigins[0] == "https://localhost:9200") { cfg.HTTP.CORS.AllowedOrigins = []string{cfg.Commons.OpenCloudURL} } + + if cfg.Metadata.SystemUserAPIKey == "" && cfg.Commons != nil && cfg.Commons.SystemUserAPIKey != "" { + cfg.Metadata.SystemUserAPIKey = cfg.Commons.SystemUserAPIKey + } + + if cfg.Metadata.SystemUserID == "" && cfg.Commons != nil && cfg.Commons.SystemUserID != "" { + cfg.Metadata.SystemUserID = cfg.Commons.SystemUserID + } } // Sanitize sanitized the configuration @@ -183,4 +201,12 @@ func Sanitize(cfg *config.Config) { cfg.Web.Config.Options.Embed.DelegateAuthenticationOrigin == "" { cfg.Web.Config.Options.Embed = nil } + + if cfg.GRPCClientTLS == nil && cfg.Commons != nil { + cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS) + } + + if cfg.GRPC.TLS == nil && cfg.Commons != nil { + cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS) + } } diff --git a/services/web/pkg/config/grpc.go b/services/web/pkg/config/grpc.go new file mode 100644 index 0000000000..44eca8a5f6 --- /dev/null +++ b/services/web/pkg/config/grpc.go @@ -0,0 +1,11 @@ +package config + +import "github.com/opencloud-eu/opencloud/pkg/shared" + +// GRPCConfig defines the available grpc configuration. +type GRPCConfig struct { + Addr string `yaml:"addr" env:"WEB_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"%%NEXT%%"` + Namespace string `yaml:"-"` + TLS *shared.GRPCServiceTLS `yaml:"tls"` + Protocol string `yaml:"protocol" env:"OC_GRPC_PROTOCOL;WEB_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service." introductionVersion:"%%NEXT%%"` +} diff --git a/services/web/pkg/fs/fs.go b/services/web/pkg/fs/fs.go new file mode 100644 index 0000000000..bb6c74e7af --- /dev/null +++ b/services/web/pkg/fs/fs.go @@ -0,0 +1,52 @@ +package fs + +import ( + "context" + "fmt" + "time" + + revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" + + "github.com/opencloud-eu/opencloud/pkg/storage/metadata" + "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" + metadataFs "github.com/opencloud-eu/opencloud/pkg/x/io/fsx/cs3/metadata" + "github.com/opencloud-eu/opencloud/services/web" + "github.com/opencloud-eu/opencloud/services/web/pkg/config" +) + +func NewThemeFS(c *config.Config) (*fsx.FallbackFS, error) { + storage, err := revaMetadata.NewCS3Storage( + c.Metadata.GatewayAddress, + c.Metadata.StorageAddress, + c.Metadata.SystemUserID, + c.Metadata.SystemUserIDP, + c.Metadata.SystemUserAPIKey, + ) + if err != nil { + return nil, err + } + + storage, err = metadata.NewLazyStorage(storage) + if err != nil { + return nil, err + } + + time.Sleep(3 * time.Second) // fixme: wait for the storage to be initialized + + if err := storage.Init(context.Background(), "web-storage"); err != nil { + return nil, err + } + + storageFS := metadataFs.NewMetadataFs(storage) + if err := storageFS.MkdirAll("assets/themes", 0755); err != nil { + return nil, fmt.Errorf("failed to create themes directory: %w", err) + } + + return fsx.NewFallbackFS( + fsx.NewBasePathFs(fsx.FromAfero(storageFS), "assets/themes"), + fsx.NewFallbackFS( + fsx.NewReadOnlyFs(fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes")), + fsx.NewReadOnlyFs(fsx.NewBasePathFs(fsx.NewOsFs(), c.Asset.ThemesPath)), + ), + ), nil +} diff --git a/services/web/pkg/server/grpc/option.go b/services/web/pkg/server/grpc/option.go new file mode 100644 index 0000000000..d83163d748 --- /dev/null +++ b/services/web/pkg/server/grpc/option.go @@ -0,0 +1,94 @@ +package grpc + +import ( + "context" + + "go.opentelemetry.io/otel/trace" + + "github.com/opencloud-eu/opencloud/pkg/log" + svc "github.com/opencloud-eu/opencloud/services/search/pkg/service/grpc/v0" + "github.com/opencloud-eu/opencloud/services/web/pkg/config" + "github.com/opencloud-eu/opencloud/services/web/pkg/metrics" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Name string + Logger log.Logger + Context context.Context + Config *config.Config + Metrics *metrics.Metrics + Handler *svc.Service + JWTSecret string + TraceProvider trace.TracerProvider +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Name provides a name for the service. +func Name(val string) Option { + return func(o *Options) { + o.Name = val + } +} + +// 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 + } +} + +// Metrics provides a function to set the metrics option. +func Metrics(val *metrics.Metrics) Option { + return func(o *Options) { + o.Metrics = val + } +} + +// Handler provides a function to set the handler option. +func Handler(val *svc.Service) Option { + return func(o *Options) { + o.Handler = val + } +} + +// JWTSecret provides a function to set the Config option. +func JWTSecret(val string) Option { + return func(o *Options) { + o.JWTSecret = val + } +} + +// TraceProvider provides a function to set the trace provider option. +func TraceProvider(val trace.TracerProvider) Option { + return func(o *Options) { + o.TraceProvider = val + } +} diff --git a/services/web/pkg/server/grpc/server.go b/services/web/pkg/server/grpc/server.go new file mode 100644 index 0000000000..b349411323 --- /dev/null +++ b/services/web/pkg/server/grpc/server.go @@ -0,0 +1,67 @@ +package grpc + +import ( + "fmt" + + "github.com/opencloud-eu/opencloud/pkg/service/grpc" + "github.com/opencloud-eu/opencloud/pkg/version" + websvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/web/v0" + "github.com/opencloud-eu/opencloud/services/web/pkg/fs" + svc "github.com/opencloud-eu/opencloud/services/web/pkg/service/grpc/v0" +) + +// Server initializes a new go-micro service ready to run +func Server(opts ...Option) (grpc.Service, error) { + options := newOptions(opts...) + + service, err := grpc.NewServiceWithClient( + options.Config.GrpcClient, + grpc.TLSEnabled(options.Config.GRPC.TLS.Enabled), + grpc.TLSCert( + options.Config.GRPC.TLS.Cert, + options.Config.GRPC.TLS.Key, + ), + grpc.Name(options.Config.Service.Name), + grpc.Context(options.Context), + grpc.Address(options.Config.GRPC.Addr), + grpc.Namespace(options.Config.GRPC.Namespace), + grpc.Logger(options.Logger), + grpc.Version(version.GetString()), + grpc.TraceProvider(options.TraceProvider), + ) + if err != nil { + options.Logger.Fatal().Err(err).Msg("Error creating web service") + return grpc.Service{}, err + } + + themeFS, err := fs.NewThemeFS(options.Config) + if err != nil { + return grpc.Service{}, fmt.Errorf("could not initialize theme filesystem: %w", err) + } + + handle, err := svc.NewHandler( + svc.Config(options.Config), + svc.Logger(options.Logger), + svc.JWTSecret(options.JWTSecret), + svc.TracerProvider(options.TraceProvider), + svc.ThemeFS(themeFS), + ) + if err != nil { + options.Logger.Error(). + Err(err). + Msg("Error initializing web service") + return grpc.Service{}, err + } + + if err := websvc.RegisterWebServiceHandler( + service.Server(), + handle, + ); err != nil { + options.Logger.Error(). + Err(err). + Msg("Error registering web provider handler") + return grpc.Service{}, err + } + + return service, nil +} diff --git a/services/web/pkg/server/http/server.go b/services/web/pkg/server/http/server.go index 25f210b13b..5714d05c95 100644 --- a/services/web/pkg/server/http/server.go +++ b/services/web/pkg/server/http/server.go @@ -5,9 +5,12 @@ import ( "path" chimiddleware "github.com/go-chi/chi/v5/middleware" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "go-micro.dev/v4" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + + "github.com/opencloud-eu/opencloud/services/web/pkg/fs" + "github.com/opencloud-eu/opencloud/pkg/cors" "github.com/opencloud-eu/opencloud/pkg/middleware" "github.com/opencloud-eu/opencloud/pkg/registry" @@ -71,10 +74,11 @@ func Server(opts ...Option) (http.Service, error) { fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.CorePath), fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/core"), ) - themeFS := fsx.NewFallbackFS( - fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.ThemesPath), - fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes"), - ) + + themeFS, err := fs.NewThemeFS(options.Config) + if err != nil { + return http.Service{}, fmt.Errorf("could not initialize theme filesystem: %w", err) + } handle, err := svc.NewService( svc.Logger(options.Logger), diff --git a/services/web/pkg/service/grpc/v0/option.go b/services/web/pkg/service/grpc/v0/option.go new file mode 100644 index 0000000000..e0903033c4 --- /dev/null +++ b/services/web/pkg/service/grpc/v0/option.go @@ -0,0 +1,68 @@ +package service + +import ( + "go.opentelemetry.io/otel/trace" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" + "github.com/opencloud-eu/opencloud/services/web/pkg/config" + "github.com/opencloud-eu/opencloud/services/web/pkg/theme" +) + +// 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 + Config *config.Config + JWTSecret string + TracerProvider trace.TracerProvider + ThemeService *theme.Service + ThemeFS *fsx.FallbackFS +} + +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 + } +} + +// Config provides a function to set the Config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} + +// JWTSecret provides a function to set the Config option. +func JWTSecret(val string) Option { + return func(o *Options) { + o.JWTSecret = val + } +} + +// TracerProvider provides a function to set the TracerProvider option +func TracerProvider(val trace.TracerProvider) Option { + return func(o *Options) { + o.TracerProvider = val + } +} + +// ThemeFS provides a function to set the themeFS option. +func ThemeFS(val *fsx.FallbackFS) Option { + return func(o *Options) { + o.ThemeFS = val + } +} diff --git a/services/web/pkg/service/grpc/v0/service.go b/services/web/pkg/service/grpc/v0/service.go new file mode 100644 index 0000000000..a305a1c6d7 --- /dev/null +++ b/services/web/pkg/service/grpc/v0/service.go @@ -0,0 +1,67 @@ +package service + +import ( + "archive/zip" + "bytes" + "context" + "fmt" + + "github.com/opencloud-eu/opencloud/pkg/log" + websvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/web/v0" + "github.com/opencloud-eu/opencloud/services/web/pkg/config" + "github.com/opencloud-eu/opencloud/services/web/pkg/theme" +) + +// NewHandler returns a service implementation for Service. +func NewHandler(opts ...Option) (websvc.WebServiceHandler, error) { + options := newOptions(opts...) + logger := options.Logger + cfg := options.Config + + themeService, err := theme.NewService( + theme.ServiceOptions{}. + WithThemeFS(options.ThemeFS.Primary()), + ) + if err != nil { + return nil, fmt.Errorf("could not initialize theme service: %w", err) + } + + return &Service{ + log: logger, + cfg: cfg, + themeService: themeService, + }, nil +} + +type Service struct { + log log.Logger + cfg *config.Config + themeService *theme.Service +} + +func (s Service) ThemeAdd(_ context.Context, req *websvc.ThemeAddRequest, res *websvc.ThemeAddResponse) error { + zr, err := zip.NewReader(bytes.NewReader(req.Data), int64(len(req.Data))) + if err != nil { + return err + } + + if err := s.themeService.Add(req.Id, zr); err != nil { + return fmt.Errorf("could not add theme %s: %w", req.Id, err) + } + + return nil +} + +func (s Service) ThemeRemove(_ context.Context, req *websvc.ThemeRemoveRequest, res *websvc.ThemeRemoveResponse) error { + if err := s.themeService.Remove(req.Id); err != nil { + return fmt.Errorf("could not remove theme %s: %w", req.Id, err) + } + + return nil +} + +func (s Service) ThemeExists(_ context.Context, req *websvc.ThemeExistsRequest, res *websvc.ThemeExistsResponse) error { + res.Exists = s.themeService.Exists(req.Id) + + return nil +} diff --git a/services/web/pkg/service/v0/service.go b/services/web/pkg/service/v0/service.go index a16ba5336f..e8a2b85f66 100644 --- a/services/web/pkg/service/v0/service.go +++ b/services/web/pkg/service/v0/service.go @@ -10,24 +10,18 @@ import ( "strings" "time" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" "github.com/go-chi/chi/v5" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/riandyrn/otelchi" "github.com/opencloud-eu/opencloud/pkg/account" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/middleware" "github.com/opencloud-eu/opencloud/pkg/tracing" - "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" "github.com/opencloud-eu/opencloud/services/web/pkg/assets" "github.com/opencloud-eu/opencloud/services/web/pkg/config" "github.com/opencloud-eu/opencloud/services/web/pkg/theme" ) -// ErrConfigInvalid is returned when the config parse is invalid. -var ErrConfigInvalid = `Invalid or missing config` - // Service defines the service handlers. type Service interface { ServeHTTP(w http.ResponseWriter, r *http.Request) @@ -50,22 +44,28 @@ func NewService(opts ...Option) (Service, error) { ), ) - svc := Web{ - logger: options.Logger, - config: options.Config, - mux: m, - coreFS: options.CoreFS, - themeFS: options.ThemeFS, - gatewaySelector: options.GatewaySelector, - } - themeService, err := theme.NewService( theme.ServiceOptions{}. - WithThemeFS(options.ThemeFS). - WithGatewaySelector(options.GatewaySelector), + WithThemeFS(options.ThemeFS), ) if err != nil { - return svc, err + return Web{}, err + } + + themeAPI, err := theme.NewHTTP( + theme.HTTPOptions{}. + WithService(themeService). + WithLogger(options.Logger), + ) + if err != nil { + return Web{}, err + } + + svc := Web{ + logger: options.Logger, + config: options.Config, + mux: m, + themeService: themeService, } m.Route(options.Config.HTTP.Root, func(r chi.Router) { @@ -75,11 +75,9 @@ func NewService(opts ...Option) (Service, error) { account.Logger(options.Logger), account.JWTSecret(options.Config.TokenManager.JWTSecret), )) - r.Post("/", themeService.LogoUpload) - r.Delete("/", themeService.LogoReset) }) r.Route("/themes", func(r chi.Router) { - r.Get("/{id}/theme.json", themeService.Get) + r.Get("/{id}/theme.json", themeAPI.Get) r.Mount("/", svc.Static( options.ThemeFS.IOFS(), path.Join(svc.config.HTTP.Root, "/themes"), @@ -92,7 +90,7 @@ func NewService(opts ...Option) (Service, error) { options.Config.HTTP.CacheTTL, )) r.Mount("/", svc.Static( - svc.coreFS, + options.CoreFS, svc.config.HTTP.Root, options.Config.HTTP.CacheTTL, )) @@ -107,12 +105,10 @@ func NewService(opts ...Option) (Service, error) { // Web defines the handlers for the web service. type Web struct { - logger log.Logger - config *config.Config - mux *chi.Mux - coreFS fs.FS - themeFS *fsx.FallbackFS - gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + logger log.Logger + config *config.Config + mux *chi.Mux + themeService *theme.Service } // ServeHTTP implements the Service interface. @@ -120,33 +116,34 @@ func (p Web) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.mux.ServeHTTP(w, r) } -func (p Web) getPayload() (payload []byte, err error) { - // render dynamically using config +// Config handles HTTP requests to provide the current configuration as a JSON response. +func (p Web) Config(w http.ResponseWriter, _ *http.Request) { + // decouple theme-related config changes + conf := *p.config + // check if the console theme exists and apply it + if conf.Web.ThemeServer == conf.Web.Config.Server && p.themeService.Exists("_console") { + conf.Web.ThemePath = path.Join("themes", "_console", "theme.json") + } // build theme url - if themeServer, err := url.Parse(p.config.Web.ThemeServer); err == nil { - p.config.Web.Config.Theme = themeServer.String() + p.config.Web.ThemePath + if themeServer, err := url.Parse(conf.Web.ThemeServer); err == nil { + themeServer.Path = conf.Web.ThemePath + conf.Web.Config.Theme = themeServer.String() } else { - p.config.Web.Config.Theme = p.config.Web.ThemePath + conf.Web.Config.Theme = conf.Web.ThemePath } - // make apps render as empty array if it is empty + // make apps render as an empty array if it is empty // TODO remove once https://github.com/golang/go/issues/27589 is fixed - if len(p.config.Web.Config.Apps) == 0 { - p.config.Web.Config.Apps = make([]string, 0) + if len(conf.Web.Config.Apps) == 0 { + conf.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) -} - -// Config implements the Service interface. -func (p Web) Config(w http.ResponseWriter, _ *http.Request) { - payload, err := p.getPayload() + payload, err := json.Marshal(conf.Web.Config) if err != nil { - http.Error(w, ErrConfigInvalid, http.StatusUnprocessableEntity) + msg := "Invalid or missing config" + p.logger.Error().Err(err).Msg(msg) + http.Error(w, msg, http.StatusUnprocessableEntity) return } diff --git a/services/web/pkg/theme/http.go b/services/web/pkg/theme/http.go new file mode 100644 index 0000000000..4612005e8e --- /dev/null +++ b/services/web/pkg/theme/http.go @@ -0,0 +1,72 @@ +package theme + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/log" +) + +// HTTPOptions defines the options to configure HTTP. +type HTTPOptions struct { + service *Service + logger log.Logger +} + +// WithService sets the service for HTTPOptions. +func (o HTTPOptions) WithService(s *Service) HTTPOptions { + o.service = s + return o +} + +// WithLogger sets the logger for the Service. +func (o HTTPOptions) WithLogger(logger log.Logger) HTTPOptions { + o.logger = logger + return o +} + +// validate validates the input parameters. +func (o HTTPOptions) validate() error { + if o.service == nil { + return errors.New("service is required") + } + + return nil +} + +type HTTP struct { + service *Service + logger log.Logger +} + +// NewHTTP initializes a new HTTP. +func NewHTTP(options HTTPOptions) (HTTP, error) { + if err := options.validate(); err != nil { + return HTTP{}, err + } + + return HTTP(options), nil +} + +// Get renders the theme for the given ID. +func (h HTTP) Get(w http.ResponseWriter, r *http.Request) { + theme, err := h.service.Build(r.PathValue("id")) + if err != nil { + h.logger.Error().Err(err).Msg("failed to merge themes") + http.Error(w, ErrBuildingThemeFailed.Error(), http.StatusInternalServerError) + } + + b, err := json.Marshal(theme) + if err != nil { + h.logger.Error().Err(err).Msg("failed to marshal theme") + http.Error(w, ErrBuildingThemeFailed.Error(), http.StatusInternalServerError) + return + } + + if _, err = w.Write(b); err != nil { + h.logger.Error().Err(err).Msg("failed to write response") + http.Error(w, ErrBuildingThemeFailed.Error(), http.StatusInternalServerError) + return + } +} diff --git a/services/web/pkg/theme/http_test.go b/services/web/pkg/theme/http_test.go new file mode 100644 index 0000000000..28a667241b --- /dev/null +++ b/services/web/pkg/theme/http_test.go @@ -0,0 +1,57 @@ +package theme_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" + "github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole" + "github.com/opencloud-eu/opencloud/services/web/pkg/theme" +) + +func TestHTTP_Get(t *testing.T) { + primaryFS := fsx.NewMemMapFs() + fallbackFS := fsx.NewFallbackFS(primaryFS, fsx.NewMemMapFs()) + + add := func(filename string, content interface{}) { + b, err := json.Marshal(content) + assert.Nil(t, err) + + assert.Nil(t, afero.WriteFile(primaryFS, filename, b, 0644)) + } + + // baseTheme + add("base/theme.json", map[string]interface{}{ + "base": "base", + }) + // brandingTheme + add("_branding/theme.json", map[string]interface{}{ + "_branding": "_branding", + }) + + service, err := theme.NewService(theme.ServiceOptions{}.WithThemeFS(fallbackFS)) + assert.NoError(t, err) + + handlers, err := theme.NewHTTP(theme.HTTPOptions{}.WithService(service)) + assert.NoError(t, err) + + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.SetPathValue("id", "base") + + w := httptest.NewRecorder() + handlers.Get(w, r) + + jsonData := gjson.Parse(w.Body.String()) + // baseTheme + assert.Equal(t, jsonData.Get("base").String(), "base") + // brandingTheme + assert.Equal(t, jsonData.Get("_branding").String(), "_branding") + // themeDefaults + assert.Equal(t, jsonData.Get("common.shareRoles."+unifiedrole.UnifiedRoleViewerID+".name").String(), "UnifiedRoleViewer") +} diff --git a/services/web/pkg/theme/kv.go b/services/web/pkg/theme/kv.go index 1c8c7f9117..981c153840 100644 --- a/services/web/pkg/theme/kv.go +++ b/services/web/pkg/theme/kv.go @@ -1,12 +1,10 @@ package theme import ( - "bytes" "encoding/json" - "strings" + "io/fs" "dario.cat/mergo" - "github.com/spf13/afero" ) // KV is a generic key-value map. @@ -26,43 +24,15 @@ func MergeKV(values ...KV) (KV, error) { return kv, nil } -// PatchKV injects the given values into to v. -func PatchKV(v map[string]interface{}, values KV) KV { - if v == nil { - v = KV{} - } - for k, val := range values { - t := v - path := strings.Split(k, ".") - for i, p := range path { - if i == len(path)-1 { - switch val { - // if the value is nil, we delete the key - case nil: - delete(t, p) - default: - t[p] = val - } - break - } - - if _, ok := t[p]; !ok { - t[p] = map[string]interface{}{} - } - - t = t[p].(map[string]interface{}) - } - } - return v -} - // LoadKV loads a key-value map from the given file system. -func LoadKV(fsys afero.Fs, p string) (KV, error) { +func LoadKV(fsys fs.FS, p string) (KV, error) { f, err := fsys.Open(p) if err != nil { return nil, err } - defer f.Close() + defer func() { + _ = f.Close() + }() var kv KV err = json.NewDecoder(f).Decode(&kv) @@ -72,27 +42,3 @@ func LoadKV(fsys afero.Fs, p string) (KV, error) { return kv, nil } - -// WriteKV writes the given key-value map to the file system. -func WriteKV(fsys afero.Fs, p string, kv KV) error { - data, err := json.Marshal(kv) - if err != nil { - return err - } - - return afero.WriteReader(fsys, p, bytes.NewReader(data)) -} - -// UpdateKV updates the key-value map at the given path with the given values. -func UpdateKV(fsys afero.Fs, p string, values KV) error { - var kv KV - - existing, err := LoadKV(fsys, p) - if err == nil { - kv = existing - } - - kv = PatchKV(kv, values) - - return WriteKV(fsys, p, kv) -} diff --git a/services/web/pkg/theme/kv_test.go b/services/web/pkg/theme/kv_test.go index 11901619d7..b3f6b780bc 100644 --- a/services/web/pkg/theme/kv_test.go +++ b/services/web/pkg/theme/kv_test.go @@ -30,82 +30,6 @@ func TestMergeKV(t *testing.T) { }) } -func TestPatchKV(t *testing.T) { - in := theme.KV{ - "a": map[string]interface{}{ - "value": "a", - }, - "b": map[string]interface{}{ - "value": "b", - }, - } - out := theme.PatchKV(in, theme.KV{ - "b.value": "b-new", - "c.value": "c-new", - "d": "d-new", - "e.value.subvalue": "e-new", - }) - assert.Equal(t, theme.KV{ - "a": map[string]interface{}{ - "value": "a", - }, - "b": map[string]interface{}{ - "value": "b-new", - }, - "c": map[string]interface{}{ - "value": "c-new", - }, - "d": "d-new", - "e": map[string]interface{}{ - "value": map[string]interface{}{ - "subvalue": "e-new", - }, - }, - }, out) -} - -func TestPatchKVUnset(t *testing.T) { - in := theme.KV{ - "a": map[string]interface{}{ - "value": "a", - }, - "b": map[string]interface{}{ - "value": "b", - }, - } - out := theme.PatchKV(in, theme.KV{ - "a.value": nil, - "b": nil, - }) - assert.Equal(t, theme.KV{ - "a": map[string]interface{}{}, - }, out) -} - -func TestPatchKVwithNil(t *testing.T) { - var in theme.KV - out := theme.PatchKV(in, theme.KV{ - "b.value": "b-new", - "c.value": "c-new", - "d": "d-new", - "e.value.subvalue": "e-new", - }) - assert.Equal(t, theme.KV{ - "b": map[string]interface{}{ - "value": "b-new", - }, - "c": map[string]interface{}{ - "value": "c-new", - }, - "d": "d-new", - "e": map[string]interface{}{ - "value": map[string]interface{}{ - "subvalue": "e-new", - }, - }, - }, out) -} - func TestLoadKV(t *testing.T) { in := theme.KV{ "a": map[string]interface{}{ @@ -121,66 +45,7 @@ func TestLoadKV(t *testing.T) { fsys := fsx.NewMemMapFs() assert.Nil(t, afero.WriteFile(fsys, "some.json", b, 0644)) - out, err := theme.LoadKV(fsys, "some.json") - assert.Nil(t, err) - assert.Equal(t, in, out) -} - -func TestWriteKV(t *testing.T) { - in := theme.KV{ - "a": map[string]interface{}{ - "value": "a", - }, - "b": map[string]interface{}{ - "value": "b", - }, - } - - fsys := fsx.NewMemMapFs() - assert.Nil(t, theme.WriteKV(fsys, "some.json", in)) - - f, err := fsys.Open("some.json") + out, err := theme.LoadKV(fsys.IOFS(), "some.json") assert.Nil(t, err) - - var out theme.KV - assert.Nil(t, json.NewDecoder(f).Decode(&out)) assert.Equal(t, in, out) } - -func TestUpdateKV(t *testing.T) { - fileKV := theme.KV{ - "a": map[string]interface{}{ - "value": "a", - }, - "b": map[string]interface{}{ - "value": "b", - }, - } - - wb, err := json.Marshal(fileKV) - assert.Nil(t, err) - - fsys := fsx.NewMemMapFs() - assert.Nil(t, afero.WriteFile(fsys, "some.json", wb, 0644)) - _ = theme.UpdateKV(fsys, "some.json", theme.KV{ - "b.value": "b-new", - "c.value": "c-new", - }) - - f, err := fsys.Open("some.json") - assert.Nil(t, err) - - var out theme.KV - assert.Nil(t, json.NewDecoder(f).Decode(&out)) - assert.Equal(t, out, theme.KV{ - "a": map[string]interface{}{ - "value": "a", - }, - "b": map[string]interface{}{ - "value": "b-new", - }, - "c": map[string]interface{}{ - "value": "c-new", - }, - }) -} diff --git a/services/web/pkg/theme/service.go b/services/web/pkg/theme/service.go index 511671614c..1bce1d29f4 100644 --- a/services/web/pkg/theme/service.go +++ b/services/web/pkg/theme/service.go @@ -1,17 +1,12 @@ package theme import ( - "encoding/json" - "net/http" + "archive/zip" + "io" + "os" + "path/filepath" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - permissionsapi "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" "github.com/pkg/errors" - "github.com/spf13/afero" - - revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" "github.com/opencloud-eu/opencloud/pkg/x/path/filepathx" @@ -19,57 +14,49 @@ import ( // ServiceOptions defines the options to configure the Service. type ServiceOptions struct { - themeFS *fsx.FallbackFS - gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + themeFS fsx.FS } // WithThemeFS sets the theme filesystem. -func (o ServiceOptions) WithThemeFS(fSys *fsx.FallbackFS) ServiceOptions { +func (o ServiceOptions) WithThemeFS(fSys fsx.FS) ServiceOptions { o.themeFS = fSys return o } -// WithGatewaySelector sets the gateway selector. -func (o ServiceOptions) WithGatewaySelector(gws pool.Selectable[gateway.GatewayAPIClient]) ServiceOptions { - o.gatewaySelector = gws - return o -} - // validate validates the input parameters. func (o ServiceOptions) validate() error { if o.themeFS == nil { return errors.New("themeFS is required") } - if o.gatewaySelector == nil { - return errors.New("gatewaySelector is required") - } - return nil } // Service defines the http service. type Service struct { - themeFS *fsx.FallbackFS - gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + themeFS fsx.FS } // NewService initializes a new Service. -func NewService(options ServiceOptions) (Service, error) { +func NewService(options ServiceOptions) (*Service, error) { if err := options.validate(); err != nil { - return Service{}, err + return nil, err } - return Service(options), nil + return &Service{ + themeFS: options.themeFS, + }, nil } -// Get renders the theme, the theme is a merge of the default theme, the base theme, and the branding theme. -func (s Service) Get(w http.ResponseWriter, r *http.Request) { +// Build builds the theme, the theme is a merge of the default theme, the base theme, and the branding theme. +func (s Service) Build(id string) (KV, error) { + themeFS := s.themeFS.IOFS() + // there is no guarantee that the theme exists, its optional; therefore, we ignore the error - baseTheme, _ := LoadKV(s.themeFS, filepathx.JailJoin(r.PathValue("id"), _themeFileName)) + baseTheme, _ := LoadKV(themeFS, filepathx.JailJoin(id, _themeFileName)) // there is no guarantee that the theme exists, its optional; therefore, we ignore the error here too - brandingTheme, _ := LoadKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName)) + brandingTheme, _ := LoadKV(themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName)) // merge the themes, the order is important, the last one wins and overrides the previous ones // themeDefaults: contains all the default values, this is guaranteed to exist @@ -78,118 +65,63 @@ func (s Service) Get(w http.ResponseWriter, r *http.Request) { // mergedTheme = themeDefaults < baseTheme < brandingTheme mergedTheme, err := MergeKV(themeDefaults, baseTheme, brandingTheme) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return nil, errors.Wrap(err, "failed to merge themes") } - b, err := json.Marshal(mergedTheme) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + return mergedTheme, nil +} - _, err = w.Write(b) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (s Service) Exists(id string) bool { + info, err := s.themeFS.Stat(filepathx.JailJoin(id, _themeFileName)) + return err == nil && !info.IsDir() && info.Size() > 0 } -// LogoUpload implements the endpoint to upload a custom logo for the OpenCloud instance. -func (s Service) LogoUpload(w http.ResponseWriter, r *http.Request) { - gatewayClient, err := s.gatewaySelector.Next() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return +func (s Service) Remove(id string) error { + if !s.Exists(id) { + return errors.Errorf("theme %s does not exist", id) } - user := revactx.ContextMustGetUser(r.Context()) - rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ - Permission: "Logo.Write", - SubjectRef: &permissionsapi.SubjectReference{ - Spec: &permissionsapi.SubjectReference_UserId{ - UserId: user.GetId(), - }, - }, - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - if rsp.GetStatus().GetCode() != rpc.Code_CODE_OK { - w.WriteHeader(http.StatusForbidden) - return - } + // remove the theme directory + return s.themeFS.RemoveAll(id) +} - file, fileHeader, err := r.FormFile("logo") - if err != nil { - if errors.Is(err, http.ErrMissingFile) { - w.WriteHeader(http.StatusBadRequest) - } - w.WriteHeader(http.StatusInternalServerError) - return +func (s Service) Add(id string, r *zip.Reader) error { + if s.Exists(id) { + return errors.Errorf("theme %s already exists", id) } - defer file.Close() - if !isFiletypePermitted(fileHeader.Filename, fileHeader.Header.Get("Content-Type")) { - w.WriteHeader(http.StatusBadRequest) - return - } + for _, f := range r.File { + filePath := filepath.Join(id, f.Name) + if f.FileInfo().IsDir() { + err := s.themeFS.MkdirAll(filePath, os.ModePerm) + if err != nil { + return err + } - fp := filepathx.JailJoin(_brandingRoot, fileHeader.Filename) - err = afero.WriteReader(s.themeFS, fp, file) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } + continue + } - err = UpdateKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName), KV{ - "common.logo": filepathx.JailJoin("themes", fp), - "clients.web.defaults.logo": filepathx.JailJoin("themes", fp), - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } + if err := s.themeFS.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } - w.WriteHeader(http.StatusOK) -} + source, err := f.Open() + if err != nil { + return err + } -// LogoReset implements the endpoint to reset the instance logo. -// The config will be changed back to use the embedded logo asset. -func (s Service) LogoReset(w http.ResponseWriter, r *http.Request) { - gatewayClient, err := s.gatewaySelector.Next() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } + dest, err := s.themeFS.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } - user := revactx.ContextMustGetUser(r.Context()) - rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ - Permission: "Logo.Write", - SubjectRef: &permissionsapi.SubjectReference{ - Spec: &permissionsapi.SubjectReference_UserId{ - UserId: user.GetId(), - }, - }, - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - if rsp.GetStatus().GetCode() != rpc.Code_CODE_OK { - w.WriteHeader(http.StatusForbidden) - return - } + if _, err := io.Copy(dest, source); err != nil { + return err + } - err = UpdateKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName), KV{ - "common.logo": nil, - "clients.web.defaults.logo": nil, - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return + _ = dest.Close() + _ = source.Close() } - w.WriteHeader(http.StatusOK) + return nil } diff --git a/services/web/pkg/theme/service_test.go b/services/web/pkg/theme/service_test.go index ca392582df..9418da4b2f 100644 --- a/services/web/pkg/theme/service_test.go +++ b/services/web/pkg/theme/service_test.go @@ -1,19 +1,11 @@ package theme_test import ( - "encoding/json" - "net/http" - "net/http/httptest" "testing" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - "github.com/spf13/afero" "github.com/stretchr/testify/assert" - "github.com/tidwall/gjson" "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" - "github.com/opencloud-eu/opencloud/services/graph/mocks" - "github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole" "github.com/opencloud-eu/opencloud/services/web/pkg/theme" ) @@ -26,49 +18,8 @@ func TestNewService(t *testing.T) { t.Run("success if the options are valid", func(t *testing.T) { _, err := theme.NewService( theme.ServiceOptions{}. - WithThemeFS(fsx.NewFallbackFS(fsx.NewMemMapFs(), fsx.NewMemMapFs())). - WithGatewaySelector(mocks.NewSelectable[gateway.GatewayAPIClient](t)), + WithThemeFS(fsx.NewFallbackFS(fsx.NewMemMapFs(), fsx.NewMemMapFs())), ) assert.NoError(t, err) }) } - -func TestService_Get(t *testing.T) { - primaryFS := fsx.NewMemMapFs() - fallbackFS := fsx.NewFallbackFS(primaryFS, fsx.NewMemMapFs()) - - add := func(filename string, content interface{}) { - b, err := json.Marshal(content) - assert.Nil(t, err) - - assert.Nil(t, afero.WriteFile(primaryFS, filename, b, 0644)) - } - - // baseTheme - add("base/theme.json", map[string]interface{}{ - "base": "base", - }) - // brandingTheme - add("_branding/theme.json", map[string]interface{}{ - "_branding": "_branding", - }) - - service, _ := theme.NewService( - theme.ServiceOptions{}. - WithThemeFS(fallbackFS). - WithGatewaySelector(mocks.NewSelectable[gateway.GatewayAPIClient](t)), - ) - r := httptest.NewRequest(http.MethodGet, "/", nil) - r.SetPathValue("id", "base") - - w := httptest.NewRecorder() - service.Get(w, r) - - jsonData := gjson.Parse(w.Body.String()) - // baseTheme - assert.Equal(t, jsonData.Get("base").String(), "base") - // brandingTheme - assert.Equal(t, jsonData.Get("_branding").String(), "_branding") - // themeDefaults - assert.Equal(t, jsonData.Get("common.shareRoles."+unifiedrole.UnifiedRoleViewerID+".name").String(), "UnifiedRoleViewer") -} diff --git a/services/web/pkg/theme/theme.go b/services/web/pkg/theme/theme.go index 56f190d04e..af23ccb788 100644 --- a/services/web/pkg/theme/theme.go +++ b/services/web/pkg/theme/theme.go @@ -1,6 +1,7 @@ package theme import ( + "errors" "path" "github.com/opencloud-eu/opencloud/pkg/capabilities" @@ -8,8 +9,9 @@ import ( ) var ( - _brandingRoot = "_branding" - _themeFileName = "theme.json" + _brandingRoot = "_branding" + _themeFileName = "theme.json" + ErrBuildingThemeFailed = errors.New("building theme failed") ) // themeDefaults contains the default values for the theme. From 979c8a13d422d7537099dd95dd19deeabc1cca4a Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Thu, 23 Apr 2026 16:17:15 +0200 Subject: [PATCH 2/2] fix: remove web theme cs3 metadata fs and use the host fs instead --- pkg/x/io/fsx/cs3/metadata/file.go | 89 ------------------------ pkg/x/io/fsx/cs3/metadata/file_info.go | 59 ---------------- pkg/x/io/fsx/cs3/metadata/fs.go | 96 -------------------------- pkg/x/io/fsx/cs3/metadata/metadata.go | 9 --- services/web/pkg/fs/fs.go | 42 ++--------- 5 files changed, 7 insertions(+), 288 deletions(-) delete mode 100644 pkg/x/io/fsx/cs3/metadata/file.go delete mode 100644 pkg/x/io/fsx/cs3/metadata/file_info.go delete mode 100644 pkg/x/io/fsx/cs3/metadata/fs.go delete mode 100644 pkg/x/io/fsx/cs3/metadata/metadata.go diff --git a/pkg/x/io/fsx/cs3/metadata/file.go b/pkg/x/io/fsx/cs3/metadata/file.go deleted file mode 100644 index 4980058e7b..0000000000 --- a/pkg/x/io/fsx/cs3/metadata/file.go +++ /dev/null @@ -1,89 +0,0 @@ -package metadata - -import ( - "bytes" - "context" - "io" - "io/fs" - "os" -) - -type File struct { - name string - fs *Fs - fileMode os.FileMode - content []byte - resource io.ReadCloser -} - -func newFile(name string, fs *Fs, fileMode os.FileMode, content []byte) (*File, error) { - return &File{ - name: name, - fs: fs, - fileMode: fileMode, - content: content, - resource: io.NopCloser(bytes.NewBuffer(content)), - }, nil -} - -func (f *File) Close() error { - return f.resource.Close() -} - -func (f *File) Read(p []byte) (n int, err error) { - return f.resource.Read(p) -} - -func (f *File) ReadAt(p []byte, off int64) (n int, err error) { - readerAt, ok := f.resource.(io.ReaderAt) - if !ok { - return -1, &fs.PathError{Op: "ReadAt", Path: f.name, Err: ErrNotImplemented} - } - - return readerAt.ReadAt(p, off) -} - -func (f *File) Seek(offset int64, whence int) (int64, error) { - seeker, ok := f.resource.(io.Seeker) - if !ok { - return -1, &fs.PathError{Op: "Seek", Path: f.name, Err: ErrNotImplemented} - } - - return seeker.Seek(offset, whence) -} - -func (f *File) Write(p []byte) (n int, err error) { - return len(p), f.fs.storage.SimpleUpload(context.Background(), f.name, p) -} - -func (f *File) WriteAt(_ []byte, _ int64) (n int, err error) { - return -1, &fs.PathError{Op: "Write", Path: f.name, Err: ErrNotImplemented} -} - -func (f *File) Name() string { - return f.name -} - -func (f *File) Readdir(_ int) ([]os.FileInfo, error) { - return nil, &fs.PathError{Op: "Readdir", Path: f.name, Err: ErrNotImplemented} -} - -func (f *File) Readdirnames(_ int) ([]string, error) { - return nil, &fs.PathError{Op: "Readdirnames", Path: f.name, Err: ErrNotImplemented} -} - -func (f *File) Sync() error { - return nil -} - -func (f *File) Truncate(_ int64) error { - return &fs.PathError{Op: "Truncate", Path: f.name, Err: ErrNotImplemented} -} - -func (f *File) WriteString(_ string) (ret int, err error) { - return -1, &fs.PathError{Op: "WriteString", Path: f.name, Err: ErrNotImplemented} -} - -func (f *File) Stat() (os.FileInfo, error) { - return newFileInfo(f.name, f.fs, f.fileMode) -} diff --git a/pkg/x/io/fsx/cs3/metadata/file_info.go b/pkg/x/io/fsx/cs3/metadata/file_info.go deleted file mode 100644 index 562ca4c193..0000000000 --- a/pkg/x/io/fsx/cs3/metadata/file_info.go +++ /dev/null @@ -1,59 +0,0 @@ -package metadata - -import ( - "context" - "io/fs" - "os" - "time" - - providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - - "github.com/opencloud-eu/reva/v2/pkg/utils" -) - -type FileInfo struct { - name string - size int64 - modTime time.Time - isDir bool - mode os.FileMode -} - -func newFileInfo(name string, fs *Fs, fileMode os.FileMode) (*FileInfo, error) { - info, err := fs.storage.Stat(context.Background(), name) - if err != nil { - return nil, err - } - - return &FileInfo{ - name: info.GetName(), - size: int64(info.GetSize()), - modTime: utils.TSToTime(info.GetMtime()), - isDir: info.GetType() == providerv1beta1.ResourceType_RESOURCE_TYPE_CONTAINER, - mode: fileMode, - }, nil -} - -func (f *FileInfo) Name() string { - return f.name -} - -func (f *FileInfo) Size() int64 { - return f.size -} - -func (f *FileInfo) ModTime() time.Time { - return f.modTime -} - -func (f *FileInfo) IsDir() bool { - return f.isDir -} - -func (f *FileInfo) Mode() fs.FileMode { - return f.mode -} - -func (f *FileInfo) Sys() any { - return nil -} diff --git a/pkg/x/io/fsx/cs3/metadata/fs.go b/pkg/x/io/fsx/cs3/metadata/fs.go deleted file mode 100644 index c62337a1c3..0000000000 --- a/pkg/x/io/fsx/cs3/metadata/fs.go +++ /dev/null @@ -1,96 +0,0 @@ -package metadata - -import ( - "context" - "fmt" - "io/fs" - "os" - "path" - "strings" - "syscall" - "time" - - "github.com/spf13/afero" - - revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata" - "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" -) - -func NewMetadataFs(storage metadata.Storage) *Fs { - return &Fs{storage: storage} -} - -type Fs struct { - storage metadata.Storage -} - -func (fs *Fs) Create(_ string) (afero.File, error) { - return nil, syscall.EPERM -} - -func (fs *Fs) Mkdir(name string, _ os.FileMode) error { - return fs.storage.MakeDirIfNotExist(context.Background(), name) -} - -func (fs *Fs) MkdirAll(name string, _ os.FileMode) error { - paths := strings.Split(name, string(os.PathSeparator)) - // Create all parent directories if they do not exist - for i := 0; i <= len(paths)-1; i++ { - c := path.Join(paths[:i+1]...) - if err := fs.storage.MakeDirIfNotExist(context.Background(), c); err != nil { - return fmt.Errorf("failed to create directory %s: %w", c, err) - } - } - - return nil -} - -func (fs *Fs) Open(name string) (afero.File, error) { - return fs.OpenFile(name, os.O_RDONLY, 0) -} - -func (fs *Fs) OpenFile(name string, _ int, _ os.FileMode) (afero.File, error) { - res, err := fs.storage.Download(context.Background(), metadata.DownloadRequest{Path: name}) - if err != nil && !revaMetadata.IsNotExist(err) { - return nil, err - } - - var contend []byte - if res != nil { - contend = res.Content - } - - return newFile(name, fs, 0, contend) -} - -func (fs *Fs) Remove(name string) error { - return fs.RemoveAll(name) -} - -func (fs *Fs) RemoveAll(path string) error { - return fs.storage.Delete(context.Background(), path) -} - -func (fs *Fs) Rename(_, _ string) error { - return syscall.EPERM -} - -func (fs *Fs) Stat(name string) (fs.FileInfo, error) { - return newFileInfo(name, fs, 0) -} - -func (fs *Fs) Name() string { - return "MetadataFS" -} - -func (fs *Fs) Chmod(_ string, _ os.FileMode) error { - return syscall.EPERM -} - -func (fs *Fs) Chown(_ string, _, _ int) error { - return syscall.EPERM -} - -func (fs *Fs) Chtimes(_ string, _ time.Time, _ time.Time) error { - return syscall.EPERM -} diff --git a/pkg/x/io/fsx/cs3/metadata/metadata.go b/pkg/x/io/fsx/cs3/metadata/metadata.go deleted file mode 100644 index aecf62fdaf..0000000000 --- a/pkg/x/io/fsx/cs3/metadata/metadata.go +++ /dev/null @@ -1,9 +0,0 @@ -package metadata - -import ( - "errors" -) - -var ( - ErrNotImplemented = errors.New("not implemented") -) diff --git a/services/web/pkg/fs/fs.go b/services/web/pkg/fs/fs.go index bb6c74e7af..f374611d76 100644 --- a/services/web/pkg/fs/fs.go +++ b/services/web/pkg/fs/fs.go @@ -1,49 +1,21 @@ package fs import ( - "context" - "fmt" - "time" - - revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata" - - "github.com/opencloud-eu/opencloud/pkg/storage/metadata" "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" - metadataFs "github.com/opencloud-eu/opencloud/pkg/x/io/fsx/cs3/metadata" "github.com/opencloud-eu/opencloud/services/web" "github.com/opencloud-eu/opencloud/services/web/pkg/config" ) +// NewThemeFS +// fixMe: since the metadataFS (cs3) is not part of the current version anymore, +// the fs nesting can (could) be simplified, I also consider to: +// - use a dedicated apace (path) for console-related things, e.g. OC_HOME/console/... instead of OC_HOME/console/... +// - maybe a memFS is good enough? Maybe not the best idea, app (web-apps) size is unknown func NewThemeFS(c *config.Config) (*fsx.FallbackFS, error) { - storage, err := revaMetadata.NewCS3Storage( - c.Metadata.GatewayAddress, - c.Metadata.StorageAddress, - c.Metadata.SystemUserID, - c.Metadata.SystemUserIDP, - c.Metadata.SystemUserAPIKey, - ) - if err != nil { - return nil, err - } - - storage, err = metadata.NewLazyStorage(storage) - if err != nil { - return nil, err - } - - time.Sleep(3 * time.Second) // fixme: wait for the storage to be initialized - - if err := storage.Init(context.Background(), "web-storage"); err != nil { - return nil, err - } - - storageFS := metadataFs.NewMetadataFs(storage) - if err := storageFS.MkdirAll("assets/themes", 0755); err != nil { - return nil, fmt.Errorf("failed to create themes directory: %w", err) - } + writableFF := fsx.NewBasePathFs(fsx.NewOsFs(), c.Asset.ThemesPath) return fsx.NewFallbackFS( - fsx.NewBasePathFs(fsx.FromAfero(storageFS), "assets/themes"), + writableFF, fsx.NewFallbackFS( fsx.NewReadOnlyFs(fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes")), fsx.NewReadOnlyFs(fsx.NewBasePathFs(fsx.NewOsFs(), c.Asset.ThemesPath)),