Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/config/defaults/thumbnails.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package defaults

// ThumbnailsMaxInputImageFileSize is the default of OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE, the
// maximum size of an image a thumbnail is made of. The thumbnails, webdav and graph services read
// that setting each, and they must agree on the default too: webdav would otherwise send images the
// thumbnails service refuses, and graph would accept space images no thumbnail is made of.
const ThumbnailsMaxInputImageFileSize = "50MB"
69 changes: 69 additions & 0 deletions services/graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,75 @@ is based on the [OData Specification](https://docs.oasis-open.org/odata/odata/v4
See the [Libre Graph API](https://docs.opencloud.eu/swagger/libre-graph-api/#/users/ListUsers) for examples
on the filters supported when querying users.

## Space Images

The image of a space is a file in the space, usually in its `.space` folder. A space manager
uploads the file, and then assigns it with `PATCH /graph/v1.0/drives/{drive-id}`, naming the id of
the file in the `image` entry of `special`:

```bash
# upload the image into the .space folder of the space; the response header
# OC-FileId holds the id of the file
curl -i -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @image.png \
"https://cloud.example.com/dav/spaces/$DRIVE_ID/.space/image.png"

# assign the file as the image of the space
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"special": [{"specialFolder": {"name": "image"}, "id": "'"$FILE_ID"'"}]}' \
"https://cloud.example.com/graph/v1.0/drives/$DRIVE_ID"
```

Clients only ever show a space image as a thumbnail. A file that is too large for the thumbnail
generator is therefore never shown at all: the space would keep a broken image, and nobody would
get an error.

So the graph service looks at the size of the file when it is assigned, and refuses a file above
the limit with `413 Request Entity Too Large`, without changing the space:

```json
{
"error": {
"code": "invalidRequest",
"message": "the space image is too large: 73400320 bytes, the maximum is 50000000 bytes",
"innererror": {
"date": "2026-11-13T15:04:05Z",
"request-id": "..."
}
}
}
```

The space keeps the image it had, as long as that is a different file. A client that overwrites the
file of the current image with the new content has replaced that content already, so the space
then shows no image.

The limit is the one of the thumbnail generator, which the graph service reads under the same names
as the webdav and thumbnails services: `OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE`, `50MB` by default.
An empty value means unlimited.

Setting the limit there sets it for all three services:

```bash
# space images, and thumbnails in general, up to 100 MB
OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE=100MB
```

`GRAPH_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE` sets a limit for space images alone. It only makes sense
to set it lower than the limit of the thumbnail generator: a space image between the two would be
accepted, but not shown.

```bash
# thumbnails up to 100 MB, but space images only up to 10 MB
OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE=100MB
GRAPH_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE=10MB
Comment on lines +116 to +118

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using an env var that clarifies which thumbnails are affected makes more sense than using the service.

Suggested change
# thumbnails up to 100 MB, but space images only up to 10 MB
OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE=100MB
GRAPH_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE=10MB
# thumbnails up to 100 MB, but space images only up to 10 MB
OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE=100MB
OC_SPACE_IMAGE_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE=10MB

And you already mentioned all the services need to agree on the thumbnail sizes. Which is confusing ...

```

Only the file size is checked. The thumbnail generator also has limits for the width and the height
of an image (`THUMBNAILS_MAX_INPUT_WIDTH` and `THUMBNAILS_MAX_INPUT_HEIGHT`), and for the number of
frames of an animation. The graph service can't check those without reading the whole image, so a
small file with too many pixels is still accepted, and still not shown. The space readme has no
size limit.

## Caching

The `graph` service can use a configured store via `GRAPH_CACHE_STORE`. Possible stores are:
Expand Down
8 changes: 6 additions & 2 deletions services/graph/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,12 @@ type Store struct {
TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_PERSISTENT_STORE_TLS_ROOT_CA_CERTIFICATE;GRAPH_STORE_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided GRAPH_STORE_TLS_INSECURE will be seen as false." introductionVersion:"7.3.0"`
}

// Thumbnails is what this service announces a preview for. The names match the
// ones the webdav service uses for the same list.
// Thumbnails is what this service announces a preview for, and which space
// images it accepts, since a space image is only ever shown as a thumbnail. The
// names match the ones the webdav service uses for the same settings.
type Thumbnails struct {
AdditionalSourceTypes []string `yaml:"additional_source_types" env:"OC_THUMBNAILS_ADDITIONAL_SOURCE_TYPES;GRAPH_THUMBNAILS_ADDITIONAL_SOURCE_TYPES" desc:"Additional mime types a preview is announced for, the same list the webdav service attempts previews for. The types the build ships are included; this only adds to them." introductionVersion:"9.0.0"`
// read under the same names as by webdav and the thumbnails service, so that
// graph doesn't accept a space image no thumbnail can be made of
MaxInputImageFileSize string `yaml:"max_input_image_file_size" env:"OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE;THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE;GRAPH_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE" desc:"The maximum file size of an image that can be set as the image of a space. A larger one is refused with HTTP 413, because no thumbnail can be made of it, and space images are only shown as thumbnails. Usable common abbreviations: [KB, KiB, MB, MiB, GB, GiB], example: 50MB. An empty value means unlimited." introductionVersion:"%%NEXT%%"`
}
4 changes: 4 additions & 0 deletions services/graph/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"
"time"

ocdefaults "github.com/opencloud-eu/opencloud/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
Expand Down Expand Up @@ -88,6 +89,9 @@ func DefaultConfig() *config.Config {
// 1 minute
UsersCacheTTL: 60,
},
Thumbnails: config.Thumbnails{
MaxInputImageFileSize: ocdefaults.ThumbnailsMaxInputImageFileSize,
},
Identity: config.Identity{
Backend: "ldap",
Metrics: config.IdentityMetrics{
Expand Down
56 changes: 56 additions & 0 deletions services/graph/pkg/config/parser/parse_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,71 @@
package parser_test

import (
"errors"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/opencloud-eu/opencloud/pkg/config/envdecode"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/parser"
thumbnailsdefaults "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/config/defaults"
thumbnailsparser "github.com/opencloud-eu/opencloud/services/thumbnails/pkg/config/parser"
)

var _ = Describe("the maximum size of a space image", func() {
BeforeEach(func() {
GinkgoT().Setenv("OC_CONFIG_DIR", GinkgoT().TempDir())
})

// only the environment, the way ParseConfig reads it: ParseConfig itself would
// also ask for secrets
decodeEnv := func() *config.Config {
cfg := defaults.DefaultConfig()
if err := envdecode.Decode(cfg); !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
Expect(err).ToNot(HaveOccurred())
}
return cfg
}

// graph would otherwise accept space images no thumbnail is made of, or
// refuse ones that would be shown
DescribeTable("is read the way the thumbnails service reads its maximum input file size",
func(env map[string]string, want string) {
for k, v := range env {
GinkgoT().Setenv(k, v)
}

cfg := decodeEnv()

generator := thumbnailsdefaults.DefaultConfig()
Expect(thumbnailsparser.ParseConfig(generator)).To(Succeed())

Expect(cfg.Thumbnails.MaxInputImageFileSize).To(Equal(want))
Expect(generator.Thumbnail.MaxInputImageFileSize).To(Equal(want))
},
Entry("from the shared name", map[string]string{
"OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE": "100MB",
}, "100MB"),
Entry("from the old name", map[string]string{
"THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE": "20MB",
}, "20MB"),
Entry("from the old name when both are set", map[string]string{
"THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE": "20MB",
"OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE": "100MB",
}, "20MB"),
)

It("can be set lower for graph alone", func() {
GinkgoT().Setenv("OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE", "100MB")
GinkgoT().Setenv("GRAPH_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE", "10MB")

Expect(decodeEnv().Thumbnails.MaxInputImageFileSize).To(Equal("10MB"))
})
})

var _ = Describe("Validate", func() {
var cfg *config.Config

Expand Down
50 changes: 48 additions & 2 deletions services/graph/pkg/service/v0/drives.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,37 @@ var (

// ErrForbiddenCharacter is thrown when the spacename contains an invalid character
ErrForbiddenCharacter = fmt.Errorf("spacenames must not contain %v", _invalidSpaceNameCharacters)

// ErrSpaceImageTooLarge is returned when the file set as the image of a space is larger than
// the maximum size of a space image
ErrSpaceImageTooLarge = errors.New("the space image is too large")
)

// checkSpaceImageSize returns ErrSpaceImageTooLarge if the file with the given
// id is larger than the maximum size of a space image. A space image is only
// ever shown as a thumbnail, and no thumbnail is made of a file above that
// size, so such an image would be stored but never shown.
func (g Graph) checkSpaceImageSize(ctx context.Context, gatewayClient gateway.GatewayAPIClient, fileID string) error {
if g.maxSpaceImageSize == 0 {
return nil
}

rid, err := storagespace.ParseID(fileID)
if err != nil {
return errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("invalid space image id %q: %v", fileID, err))
}

res, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &rid}})
if err := errorcode.FromStat(res, err); err != nil {
return err
}

if size := res.GetInfo().GetSize(); size > g.maxSpaceImageSize {
return fmt.Errorf("%w: %d bytes, the maximum is %d bytes", ErrSpaceImageTooLarge, size, g.maxSpaceImageSize)
}
return nil
}

// GetDrives serves as a factory method that returns the appropriate
// http.Handler function based on the specified API version.
func (g Graph) GetDrives(version APIVersion) http.HandlerFunc {
Expand Down Expand Up @@ -576,9 +605,26 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
}

for _, special := range drive.Special {
if special.Id != nil {
updateSpaceRequest.StorageSpace.Opaque = utils.AppendPlainToOpaque(updateSpaceRequest.StorageSpace.Opaque, *special.SpecialFolder.Name, *special.Id)
// an entry without an id names no file to set, so it changes nothing, as it always did;
// the web UI, for one, sends such an entry after it has deleted the file of the space image
if special.Id == nil {
continue
}

// an empty id names no file, so there is nothing to look at
if special.SpecialFolder.GetName() == SpaceImageSpecialFolderName && *special.Id != "" {
if err := g.checkSpaceImageSize(r.Context(), gatewayClient, *special.Id); err != nil {
log.Warn().Err(err).Str("image", *special.Id).Msg("could not update drive: space image refused")
if errors.Is(err, ErrSpaceImageTooLarge) {
errorcode.InvalidRequest.Render(w, r, http.StatusRequestEntityTooLarge, err.Error())
return
}
errorcode.RenderError(w, r, err)
return
}
}

updateSpaceRequest.StorageSpace.Opaque = utils.AppendPlainToOpaque(updateSpaceRequest.StorageSpace.Opaque, *special.SpecialFolder.Name, *special.Id)
}

if _, ok := drive.GetNameOk(); ok {
Expand Down
Loading