Repository: owncloud/ocis
Pull request #3289 (2022-03-08 22:38:02)

[tests-only] ApiTest: set space image/description to space
Created tests for method: 
PATCH `https://localhost:9200/graph/v1.0/drives/spaceId --data-raw '{"special":[{"specialFolder":{"name":"readme"},"id":"fileId"}]}'`

1. I create a readmi file or an image file in the .space folder
2. I add the file to a special section in space (using method PATCH)
3. I check that the files are added to the space response

<img width="522" alt="Screenshot 2022-03-08 at 23 37 04" src="https://user-images.githubusercontent.com/84779829/157337743-9da28cb1-69b2-4ae7-b9b4-3e2d5c9bc08e.png">



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e99ab484e82..bcf1d91ba4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ The following sections list the changes for unreleased.
 ## Summary
 
 * Enhancement - Include etags in drives listing: [#3267](https://github.com/owncloud/ocis/pull/3267)
+* Enhancement - Improve thumbnails API: [#3272](https://github.com/owncloud/ocis/pull/3272)
 
 ## Details
 
@@ -15,6 +16,14 @@ The following sections list the changes for unreleased.
    Added etags in the response of list drives.
 
    https://github.com/owncloud/ocis/pull/3267
+
+* Enhancement - Improve thumbnails API: [#3272](https://github.com/owncloud/ocis/pull/3272)
+
+   Changed the thumbnails API to no longer transfer images via GRPC. GRPC has a limited message
+   size and isn't very efficient with large binary data. The new API transports the images over
+   HTTP.
+
+   https://github.com/owncloud/ocis/pull/3272
 # Changelog for [1.18.0] (2022-03-03)
 
 The following sections list the changes for 1.18.0.
diff --git a/changelog/unreleased/thumbnails-api.md b/changelog/unreleased/thumbnails-api.md
new file mode 100644
index 00000000000..9ba858b0738
--- /dev/null
+++ b/changelog/unreleased/thumbnails-api.md
@@ -0,0 +1,7 @@
+Enhancement: Improve thumbnails API
+
+Changed the thumbnails API to no longer transfer images via GRPC.
+GRPC has a limited message size and isn't very efficient with large binary data.
+The new API transports the images over HTTP.
+
+https://github.com/owncloud/ocis/pull/3272
diff --git a/go.mod b/go.mod
index 3f0e352d70c..98d274fb742 100644
--- a/go.mod
+++ b/go.mod
@@ -41,7 +41,7 @@ require (
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.8.0
 	github.com/iancoleman/strcase v0.2.0
 	github.com/justinas/alice v1.2.0
-	github.com/libregraph/idm v0.3.1-0.20220222123017-f9d520ac1f11
+	github.com/libregraph/idm v0.3.1-0.20220309072604-7a05922a056f
 	github.com/libregraph/lico v0.53.1
 	github.com/mennanov/fieldmask-utils v0.5.0
 	github.com/mitchellh/mapstructure v1.4.3
diff --git a/go.sum b/go.sum
index 1d924b2c008..a622e4d1858 100644
--- a/go.sum
+++ b/go.sum
@@ -939,8 +939,8 @@ github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
 github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/libregraph/idm v0.3.1-0.20220222123017-f9d520ac1f11 h1:ReFrghRLQhQdt5BW7HXtEzySmsgUCXL1U36gnmka73U=
-github.com/libregraph/idm v0.3.1-0.20220222123017-f9d520ac1f11/go.mod h1:wB8ApGAQsXDXXGWEeFimK/3q8Z/jRa+m3nqnvoYjBsQ=
+github.com/libregraph/idm v0.3.1-0.20220309072604-7a05922a056f h1:FKa0JYnkhto9Z4UsQHdEM/bxRklqiICaOHhGN+s9cnI=
+github.com/libregraph/idm v0.3.1-0.20220309072604-7a05922a056f/go.mod h1:wB8ApGAQsXDXXGWEeFimK/3q8Z/jRa+m3nqnvoYjBsQ=
 github.com/libregraph/lico v0.53.1 h1:c5JYFg0mrIgk6Ktj77rNJ2K8X01hs28JJ9Vw1N1iMAk=
 github.com/libregraph/lico v0.53.1/go.mod h1:EXf6Y9s9TygW6unSXosBpVXWUURoN2sPX0053o4zVfQ=
 github.com/linode/linodego v0.25.3/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE=
diff --git a/idm/idm.go b/idm/idm.go
new file mode 100644
index 00000000000..be4a60e5457
--- /dev/null
+++ b/idm/idm.go
@@ -0,0 +1,11 @@
+package idm
+
+import (
+	_ "embed"
+)
+
+//go:embed ldif/base.ldif.tmpl
+var BaseLDIF string
+
+//go:embed ldif/demousers.ldif
+var DemoUsersLDIF string
diff --git a/idm/ldif/base.ldif.tmpl b/idm/ldif/base.ldif.tmpl
new file mode 100644
index 00000000000..5c290ad17a7
--- /dev/null
+++ b/idm/ldif/base.ldif.tmpl
@@ -0,0 +1,24 @@
+dn: o=libregraph-idm
+o: libregraph-idm
+objectClass: organization
+
+dn: ou=users,o=libregraph-idm
+objectClass: organizationalUnit
+ou: users
+
+dn: ou=sysusers,o=libregraph-idm
+objectClass: organizationalUnit
+ou: users
+
+dn: ou=groups,o=libregraph-idm
+objectClass: organizationalUnit
+ou: groups
+
+{{ range . -}}
+dn: uid={{ .Name }},ou=sysusers,o=libregraph-idm
+objectClass: account
+objectClass: simpleSecurityObject
+uid: {{ .Name }}
+userPassword:: {{ .Password }}
+
+{{ end -}}
diff --git a/idm/ldif/demousers.ldif b/idm/ldif/demousers.ldif
new file mode 100644
index 00000000000..8bfdf2461b2
--- /dev/null
+++ b/idm/ldif/demousers.ldif
@@ -0,0 +1,162 @@
+dn: uid=einstein,ou=users,o=libregraph-idm
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: ownCloud
+objectClass: person
+objectClass: top
+uid: einstein
+givenName: Albert
+sn: Einstein
+cn: einstein
+displayName: Albert Einstein
+description: A German-born theoretical physicist who developed the theory of relativity, one of the two pillars of modern physics (alongside quantum mechanics).
+mail: einstein@example.org
+ownCloudUUID: 4c510ada-c86b-4815-8820-42cdf82c3d51
+userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTIkOFpyclR0NXA1a0VmVlhL
+ akNHaVBEUSRnemZCWWwrTHdzTUhXQWJSMEJ2NnRiZk1XZjZaOVJ0Mms5Z3VkSWJ5bzg4
+
+dn: uid=marie,ou=users,o=libregraph-idm
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: ownCloud
+objectClass: person
+objectClass: top
+uid: marie
+givenName: Marie
+sn: Curie
+cn: marie
+displayName: Marie Skłodowska Curie
+description: A Polish and naturalized-French physicist and chemist who conducted pioneering research on radioactivity.
+mail: marie@example.org
+ownCloudUUID: f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c
+userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTIkUHJzWkpQQW9pMkFwZHlJ
+ a2Q1NGkzQSRnalZzR3doTmk2K0djenJ4SVdPalN2UlBpWXhKSXpHVG4vcnpQZzkvSlZN
+
+dn: uid=richard,ou=users,o=libregraph-idm
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: ownCloud
+objectClass: person
+objectClass: top
+uid: richard
+givenName: Richard
+sn: Feynman
+cn: richard
+displayName: Richard Phillips Feynman
+description: An American theoretical physicist, known for his work in the path integral formulation of quantum mechanics, the theory of quantum electrodynamics, the physics of the superfluidity of supercooled liquid helium, as well as his work in particle physics for which he proposed the parton model.
+mail: richard@example.org
+ownCloudUUID: 932b4540-8d16-481e-8ef4-588e4b6b151c
+userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTIkNjlNcUQxem5sUUZ2SUha
+ d2dxU00xQSRVQmNEa2NDZktMemVpQnlyb0JjOTdCSVRhTFo2WjZIL2dhbytSTVh6OHhn
+
+dn: uid=moss,ou=users,o=libregraph-idm
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: ownCloud
+objectClass: person
+objectClass: top
+uid: moss
+givenName: Maurice
+sn: Moss
+cn: moss
+displayName: Maurice Moss
+description: A worker in the IT Department of Reynholm Industries. Of all the working staff in the IT Department, he is the most hard-working, the most experienced, and the most capable of doing his job well. He puts a lot of effort into his work, however he does not get the credit he deserves.
+mail: moss@example.org
+ownCloudUUID: 058bff95-6708-4fe5-91e4-9ea3d377588b
+userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTIkZU0xaXR6amQ2dlNSSERx
+ NlZCbXBlQSQxNzBhcTB3YjJZZ2NLU2cwWDhHY3l6ckZwMUllcGplMTNraDdVNjUyNXk4
+
+dn: uid=admin,ou=users,o=libregraph-idm
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: ownCloud
+objectClass: person
+objectClass: top
+uid: admin
+givenName: Admin
+sn: Admin
+cn: admin
+displayName: Admin
+description: An admin for this oCIS instance.
+mail: admin@example.org
+ownCloudUUID: ddc2004c-0977-11eb-9d3f-a793888cd0f8
+userPassword:: e0FSR09OMn0kYXJnb24yaWQkdj0xOSRtPTY1NTM2LHQ9MSxwPTIkRXdwYUhJeVErcG9wdkcv
+ Tk81R0o2USRNWHp4czNvdHBhOWp3S0hxc1lLMlZodzAralUxSFowMUNpOXducWZlT1pn
+
+dn: cn=users,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: users
+description: Users
+ownCloudUUID: 509a9dcd-bb37-4f4f-a01a-19dca27d9cfa
+member: uid=einstein,ou=users,o=libregraph-idm
+member: uid=marie,ou=users,o=libregraph-idm
+member: uid=richard,ou=users,o=libregraph-idm
+member: uid=moss,ou=users,o=libregraph-idm
+member: uid=admin,ou=users,o=libregraph-idm
+
+dn: cn=sailing-lovers,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: sailing-lovers
+description: Sailing lovers
+ownCloudUUID: 6040aa17-9c64-4fef-9bd0-77234d71bad0
+member: uid=einstein,ou=users,o=libregraph-idm
+
+dn: cn=violin-haters,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: violin-haters
+description: Violin haters
+ownCloudUUID: dd58e5ec-842e-498b-8800-61f2ec6f911f
+member: uid=einstein,ou=users,o=libregraph-idm
+
+dn: cn=radium-lovers,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: radium-lovers
+description: Radium lovers
+ownCloudUUID: 7b87fd49-286e-4a5f-bafd-c535d5dd997a
+member: uid=marie,ou=users,o=libregraph-idm
+
+dn: cn=polonium-lovers,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: polonium-lovers
+description: Polonium lovers
+ownCloudUUID: cedc21aa-4072-4614-8676-fa9165f598ff
+member: uid=marie,ou=users,o=libregraph-idm
+
+dn: cn=quantum-lovers,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: quantum-lovers
+description: Quantum lovers
+ownCloudUUID: a1726108-01f8-4c30-88df-2b1a9d1cba1a
+member: uid=richard,ou=users,o=libregraph-idm
+
+dn: cn=philosophy-haters,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: philosophy-haters
+description: Philosophy haters
+ownCloudUUID: 167cbee2-0518-455a-bfb2-031fe0621e5d
+member: uid=richard,ou=users,o=libregraph-idm
+
+dn: cn=physics-lovers,ou=groups,o=libregraph-idm
+objectClass: groupOfNames
+objectClass: ownCloud
+objectClass: top
+cn: physics-lovers
+description: Physics lovers
+ownCloudUUID: 262982c1-2362-4afa-bfdf-8cbfef64a06e
+member: uid=einstein,ou=users,o=libregraph-idm
+member: uid=marie,ou=users,o=libregraph-idm
+member: uid=richard,ou=users,o=libregraph-idm
diff --git a/idm/pkg/command/server.go b/idm/pkg/command/server.go
index adfe31850a4..6306767c678 100644
--- a/idm/pkg/command/server.go
+++ b/idm/pkg/command/server.go
@@ -13,6 +13,7 @@ import (
 	"github.com/libregraph/idm/pkg/ldappassword"
 	"github.com/libregraph/idm/pkg/ldbbolt"
 	"github.com/libregraph/idm/server"
+	"github.com/owncloud/ocis/idm"
 	"github.com/owncloud/ocis/idm/pkg/config"
 	"github.com/owncloud/ocis/idm/pkg/config/parser"
 	"github.com/owncloud/ocis/idm/pkg/logging"
@@ -53,7 +54,7 @@ func start(ctx context.Context, logger log.Logger, cfg *config.Config) error {
 		TLSCertFile:     cfg.IDM.Cert,
 		TLSKeyFile:      cfg.IDM.Key,
 		LDAPBaseDN:      "o=libregraph-idm",
-		LDAPAdminDN:     "uid=libregrah,o=libregraph-idm",
+		LDAPAdminDN:     "uid=libregraph,ou=sysusers,o=libregraph-idm",
 
 		BoltDBFile: cfg.IDM.DatabasePath,
 	}
@@ -80,15 +81,26 @@ func start(ctx context.Context, logger log.Logger, cfg *config.Config) error {
 
 func bootstrap(logger log.Logger, cfg *config.Config, srvcfg server.Config) error {
 	// Hash password if the config does not supply a hash already
-	var pwhash string
 	var err error
-	if strings.HasPrefix(cfg.IDM.AdminPassword, "$argon2id$") {
-		// password is alread hashed
-		pwhash = "{ARGON2}" + cfg.IDM.AdminPassword
-	} else {
-		if pwhash, err = ldappassword.Hash(cfg.IDM.AdminPassword, "{ARGON2}"); err != nil {
-			return err
-		}
+
+	type svcUser struct {
+		Name     string
+		Password string
+	}
+
+	serviceUsers := []svcUser{
+		{
+			Name:     "libregraph",
+			Password: cfg.ServiceUserPasswords.IdmAdmin,
+		},
+		{
+			Name:     "idp",
+			Password: cfg.ServiceUserPasswords.Idp,
+		},
+		{
+			Name:     "reva",
+			Password: cfg.ServiceUserPasswords.Reva,
+		},
 	}
 
 	bdb := &ldbbolt.LdbBolt{}
@@ -103,22 +115,38 @@ func bootstrap(logger log.Logger, cfg *config.Config, srvcfg server.Config) erro
 	}
 
 	// Prepare the initial Data from template. To be able to set the
-	// supplied admin password
-	tmpl, err := template.New("baseldif").Parse(baseldif)
+	// supplied service user passwords
+	tmpl, err := template.New("baseldif").Parse(idm.BaseLDIF)
 	if err != nil {
 		return err
 	}
 
+	for i := range serviceUsers {
+		if strings.HasPrefix(serviceUsers[i].Password, "$argon2id$") {
+			// password is alread hashed
+			serviceUsers[i].Password = "{ARGON2}" + serviceUsers[i].Password
+		} else {
+			if serviceUsers[i].Password, err = ldappassword.Hash(serviceUsers[i].Password, "{ARGON2}"); err != nil {
+				return err
+			}
+		}
+		// We need to treat the hash as binary in the LDIF template to avoid
+		// go-ldap/ldif to to any fancy escaping
+		serviceUsers[i].Password = base64.StdEncoding.EncodeToString([]byte(serviceUsers[i].Password))
+	}
 	var tmplWriter strings.Builder
-	// We need to treat the hash as binary in the LDIF template to avoid
-	// go-ldap/ldif to to any fancy escaping
-	b64 := base64.StdEncoding.EncodeToString([]byte(pwhash))
-	err = tmpl.Execute(&tmplWriter, b64)
+	err = tmpl.Execute(&tmplWriter, serviceUsers)
 	if err != nil {
 		return err
 	}
 
-	s := strings.NewReader(tmplWriter.String())
+	bootstrapData := tmplWriter.String()
+
+	if cfg.CreateDemoUsers {
+		bootstrapData = bootstrapData + "\n" + idm.DemoUsersLDIF
+	}
+
+	s := strings.NewReader(bootstrapData)
 	lf := &ldif.LDIF{}
 	err = ldif.Unmarshal(s, lf)
 	if err != nil {
@@ -131,23 +159,6 @@ func bootstrap(logger log.Logger, cfg *config.Config, srvcfg server.Config) erro
 			return fmt.Errorf("error adding Entry '%s': %w", entry.DN, err)
 		}
 	}
+
 	return nil
 }
-
-var baseldif string = `dn: o=libregraph-idm
-o: libregraph-idm
-objectClass: organization
-
-dn: ou=users,o=libregraph-idm
-objectClass: organizationalUnit
-ou: users
-
-dn: ou=groups,o=libregraph-idm
-objectClass: organizationalUnit
-ou: groups
-
-dn: uid=libregraph,o=libregraph-idm
-objectClass: account
-objectClass: simpleSecurityObject
-uid: libregraph
-userPassword:: {{.}}`
diff --git a/idm/pkg/config/config.go b/idm/pkg/config/config.go
index da0fd1bcab5..44a97887c89 100644
--- a/idm/pkg/config/config.go
+++ b/idm/pkg/config/config.go
@@ -16,15 +16,23 @@ type Config struct {
 	Log     *Log     `ocisConfig:"log"`
 	Debug   Debug    `ocisConfig:"debug"`
 
-	IDM Settings `ocisConfig:"idm"`
+	IDM             Settings `ocisConfig:"idm"`
+	CreateDemoUsers bool     `ocisConfig:"create_demo_users" env:"IDM_CREATE_DEMO_USERS;ACCOUNTS_DEMO_USERS_AND_GROUPS" desc:"Flag to enabe/disable the creation of the demo users"`
+
+	ServiceUserPasswords ServiceUserPasswords `ocisConfig:"service_user_passwords"`
 
 	Context context.Context
 }
 
 type Settings struct {
-	LDAPSAddr     string `ocisConfig:"ldaps_addr" env:"IDM_LDAPS_ADDR"`
-	Cert          string `ocisConfig:"cert" env:"IDM_LDAPS_CERT"`
-	Key           string `ocisConfig:"cert" env:"IDM_LDAPS_KEY"`
-	DatabasePath  string `ocisConfig:"database" env:"IDM_DATABASE_PATH"`
-	AdminPassword string `ocisConfig:"admin_password" env:"IDM_ADMIN_PASSWORD"`
+	LDAPSAddr    string `ocisConfig:"ldaps_addr" env:"IDM_LDAPS_ADDR" desc:"Listen address for the ldaps listener (ip-addr:port)"`
+	Cert         string `ocisConfig:"cert" env:"IDM_LDAPS_CERT" desc:"File name of the TLS server certificate for the ldaps listener"`
+	Key          string `ocisConfig:"cert" env:"IDM_LDAPS_KEY" desc:"File name for the TLS certificate key for the server certificate"`
+	DatabasePath string `ocisConfig:"database" env:"IDM_DATABASE_PATH" desc:"Full path to the idm backend database"`
+}
+
+type ServiceUserPasswords struct {
+	IdmAdmin string `ocisConfig:"admin_password" env:"IDM_ADMIN_PASSWORD" desc:"Password to set for the \"idm\" service users. Either cleartext or an argon2id hash"`
+	Reva     string `ocisConfig:"reva_password" env:"IDM_REVASVC_PASSWORD" desc:"Password to set for the \"reva\" service users. Either cleartext or an argon2id hash"`
+	Idp      string `ocisConfig:"idp_password" env:"IDM_IDPSVC_PASSWORD" desc:"Password to set for the \"idp\" service users. Either cleartext or an argon2id hash"`
 }
diff --git a/idm/pkg/config/defaultconfig.go b/idm/pkg/config/defaultconfig.go
index aaec93bdea1..b82869425f0 100644
--- a/idm/pkg/config/defaultconfig.go
+++ b/idm/pkg/config/defaultconfig.go
@@ -11,12 +11,17 @@ func DefaultConfig() *Config {
 		Service: Service{
 			Name: "idm",
 		},
+		CreateDemoUsers: true,
+		ServiceUserPasswords: ServiceUserPasswords{
+			IdmAdmin: "idm",
+			Idp:      "idp",
+			Reva:     "reva",
+		},
 		IDM: Settings{
-			LDAPSAddr:     "127.0.0.1:9235",
-			Cert:          path.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
-			Key:           path.Join(defaults.BaseDataPath(), "idm", "ldap.key"),
-			DatabasePath:  path.Join(defaults.BaseDataPath(), "idm", "ocis.boltdb"),
-			AdminPassword: "admin",
+			LDAPSAddr:    "127.0.0.1:9235",
+			Cert:         path.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
+			Key:          path.Join(defaults.BaseDataPath(), "idm", "ldap.key"),
+			DatabasePath: path.Join(defaults.BaseDataPath(), "idm", "ocis.boltdb"),
 		},
 	}
 }
diff --git a/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go b/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go
index 1fab202edf7..25721838c88 100644
--- a/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go
+++ b/protogen/gen/ocis/messages/thumbnails/v0/thumbnails.pb.go
@@ -20,6 +20,56 @@ const (
 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 )
 
+// The file types to which the thumbnail can be encoded to.
+type ThumbnailType int32
+
+const (
+	ThumbnailType_PNG ThumbnailType = 0 // Represents PNG type
+	ThumbnailType_JPG ThumbnailType = 1 // Represents JPG type
+	ThumbnailType_GIF ThumbnailType = 2 // Represents GIF type
+)
+
+// Enum value maps for ThumbnailType.
+var (
+	ThumbnailType_name = map[int32]string{
+		0: "PNG",
+		1: "JPG",
+		2: "GIF",
+	}
+	ThumbnailType_value = map[string]int32{
+		"PNG": 0,
+		"JPG": 1,
+		"GIF": 2,
+	}
+)
+
+func (x ThumbnailType) Enum() *ThumbnailType {
+	p := new(ThumbnailType)
+	*p = x
+	return p
+}
+
+func (x ThumbnailType) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (ThumbnailType) Descriptor() protoreflect.EnumDescriptor {
+	return file_ocis_messages_thumbnails_v0_thumbnails_proto_enumTypes[0].Descriptor()
+}
+
+func (ThumbnailType) Type() protoreflect.EnumType {
+	return &file_ocis_messages_thumbnails_v0_thumbnails_proto_enumTypes[0]
+}
+
+func (x ThumbnailType) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use ThumbnailType.Descriptor instead.
+func (ThumbnailType) EnumDescriptor() ([]byte, []int) {
+	return file_ocis_messages_thumbnails_v0_thumbnails_proto_rawDescGZIP(), []int{0}
+}
+
 type WebdavSource struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -184,12 +234,15 @@ var file_ocis_messages_thumbnails_v0_thumbnails_proto_rawDesc = []byte{
 	0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70,
 	0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
 	0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68,
-	0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x43, 0x5a, 0x41, 0x67, 0x69, 0x74,
-	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
-	0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67,
-	0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73,
-	0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x76, 0x30, 0x62, 0x06,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x2a, 0x0a, 0x0d, 0x54, 0x68, 0x75,
+	0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x50, 0x4e,
+	0x47, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4a, 0x50, 0x47, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03,
+	0x47, 0x49, 0x46, 0x10, 0x02, 0x42, 0x43, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
+	0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69,
+	0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f,
+	0x63, 0x69, 0x73, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x74, 0x68, 0x75,
+	0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
 }
 
 var (
@@ -204,10 +257,12 @@ func file_ocis_messages_thumbnails_v0_thumbnails_proto_rawDescGZIP() []byte {
 	return file_ocis_messages_thumbnails_v0_thumbnails_proto_rawDescData
 }
 
+var file_ocis_messages_thumbnails_v0_thumbnails_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
 var file_ocis_messages_thumbnails_v0_thumbnails_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
 var file_ocis_messages_thumbnails_v0_thumbnails_proto_goTypes = []interface{}{
-	(*WebdavSource)(nil), // 0: ocis.messages.thumbnails.v0.WebdavSource
-	(*CS3Source)(nil),    // 1: ocis.messages.thumbnails.v0.CS3Source
+	(ThumbnailType)(0),   // 0: ocis.messages.thumbnails.v0.ThumbnailType
+	(*WebdavSource)(nil), // 1: ocis.messages.thumbnails.v0.WebdavSource
+	(*CS3Source)(nil),    // 2: ocis.messages.thumbnails.v0.CS3Source
 }
 var file_ocis_messages_thumbnails_v0_thumbnails_proto_depIdxs = []int32{
 	0, // [0:0] is the sub-list for method output_type
@@ -253,13 +308,14 @@ func file_ocis_messages_thumbnails_v0_thumbnails_proto_init() {
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_ocis_messages_thumbnails_v0_thumbnails_proto_rawDesc,
-			NumEnums:      0,
+			NumEnums:      1,
 			NumMessages:   2,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
 		GoTypes:           file_ocis_messages_thumbnails_v0_thumbnails_proto_goTypes,
 		DependencyIndexes: file_ocis_messages_thumbnails_v0_thumbnails_proto_depIdxs,
+		EnumInfos:         file_ocis_messages_thumbnails_v0_thumbnails_proto_enumTypes,
 		MessageInfos:      file_ocis_messages_thumbnails_v0_thumbnails_proto_msgTypes,
 	}.Build()
 	File_ocis_messages_thumbnails_v0_thumbnails_proto = out.File
diff --git a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go
index cb2dd1d8639..cf949fb4563 100644
--- a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go
+++ b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.pb.go
@@ -22,53 +22,6 @@ const (
 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 )
 
-// The file types to which the thumbnail can get encoded to.
-type GetThumbnailRequest_ThumbnailType int32
-
-const (
-	GetThumbnailRequest_PNG GetThumbnailRequest_ThumbnailType = 0 // Represents PNG type
-	GetThumbnailRequest_JPG GetThumbnailRequest_ThumbnailType = 1 // Represents JPG type
-)
-
-// Enum value maps for GetThumbnailRequest_ThumbnailType.
-var (
-	GetThumbnailRequest_ThumbnailType_name = map[int32]string{
-		0: "PNG",
-		1: "JPG",
-	}
-	GetThumbnailRequest_ThumbnailType_value = map[string]int32{
-		"PNG": 0,
-		"JPG": 1,
-	}
-)
-
-func (x GetThumbnailRequest_ThumbnailType) Enum() *GetThumbnailRequest_ThumbnailType {
-	p := new(GetThumbnailRequest_ThumbnailType)
-	*p = x
-	return p
-}
-
-func (x GetThumbnailRequest_ThumbnailType) String() string {
-	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (GetThumbnailRequest_ThumbnailType) Descriptor() protoreflect.EnumDescriptor {
-	return file_ocis_services_thumbnails_v0_thumbnails_proto_enumTypes[0].Descriptor()
-}
-
-func (GetThumbnailRequest_ThumbnailType) Type() protoreflect.EnumType {
-	return &file_ocis_services_thumbnails_v0_thumbnails_proto_enumTypes[0]
-}
-
-func (x GetThumbnailRequest_ThumbnailType) Number() protoreflect.EnumNumber {
-	return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use GetThumbnailRequest_ThumbnailType.Descriptor instead.
-func (GetThumbnailRequest_ThumbnailType) EnumDescriptor() ([]byte, []int) {
-	return file_ocis_services_thumbnails_v0_thumbnails_proto_rawDescGZIP(), []int{0, 0}
-}
-
 // A request to retrieve a thumbnail
 type GetThumbnailRequest struct {
 	state         protoimpl.MessageState
@@ -78,7 +31,7 @@ type GetThumbnailRequest struct {
 	// The path to the source image
 	Filepath string `protobuf:"bytes,1,opt,name=filepath,proto3" json:"filepath,omitempty"`
 	// The type to which the thumbnail should get encoded to.
-	ThumbnailType GetThumbnailRequest_ThumbnailType `protobuf:"varint,2,opt,name=thumbnail_type,json=thumbnailType,proto3,enum=ocis.services.thumbnails.v0.GetThumbnailRequest_ThumbnailType" json:"thumbnail_type,omitempty"`
+	ThumbnailType v0.ThumbnailType `protobuf:"varint,2,opt,name=thumbnail_type,json=thumbnailType,proto3,enum=ocis.messages.thumbnails.v0.ThumbnailType" json:"thumbnail_type,omitempty"`
 	// The width of the thumbnail
 	Width int32 `protobuf:"varint,3,opt,name=width,proto3" json:"width,omitempty"`
 	// The height of the thumbnail
@@ -128,11 +81,11 @@ func (x *GetThumbnailRequest) GetFilepath() string {
 	return ""
 }
 
-func (x *GetThumbnailRequest) GetThumbnailType() GetThumbnailRequest_ThumbnailType {
+func (x *GetThumbnailRequest) GetThumbnailType() v0.ThumbnailType {
 	if x != nil {
 		return x.ThumbnailType
 	}
-	return GetThumbnailRequest_PNG
+	return v0.ThumbnailType(0)
 }
 
 func (x *GetThumbnailRequest) GetWidth() int32 {
@@ -192,10 +145,12 @@ type GetThumbnailResponse struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	// The thumbnail as a binary
-	Thumbnail []byte `protobuf:"bytes,1,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"`
+	// The endpoint where the thumbnail can be downloaded.
+	DataEndpoint string `protobuf:"bytes,1,opt,name=data_endpoint,json=dataEndpoint,proto3" json:"data_endpoint,omitempty"`
+	// The transfer token to be able to download the thumbnail.
+	TransferToken string `protobuf:"bytes,2,opt,name=transfer_token,json=transferToken,proto3" json:"transfer_token,omitempty"`
 	// The mimetype of the thumbnail
-	Mimetype string `protobuf:"bytes,2,opt,name=mimetype,proto3" json:"mimetype,omitempty"`
+	Mimetype string `protobuf:"bytes,3,opt,name=mimetype,proto3" json:"mimetype,omitempty"`
 }
 
 func (x *GetThumbnailResponse) Reset() {
@@ -230,11 +185,18 @@ func (*GetThumbnailResponse) Descriptor() ([]byte, []int) {
 	return file_ocis_services_thumbnails_v0_thumbnails_proto_rawDescGZIP(), []int{1}
 }
 
-func (x *GetThumbnailResponse) GetThumbnail() []byte {
+func (x *GetThumbnailResponse) GetDataEndpoint() string {
 	if x != nil {
-		return x.Thumbnail
+		return x.DataEndpoint
 	}
-	return nil
+	return ""
+}
+
+func (x *GetThumbnailResponse) GetTransferToken() string {
+	if x != nil {
+		return x.TransferToken
+	}
+	return ""
 }
 
 func (x *GetThumbnailResponse) GetMimetype() string {
@@ -257,69 +219,69 @@ var file_ocis_services_thumbnails_v0_thumbnails_proto_rawDesc = []byte{
 	0x69, 0x6c, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x8e, 0x03, 0x0a, 0x13, 0x47, 0x65,
+	0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd7, 0x02, 0x0a, 0x13, 0x47, 0x65,
 	0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
 	0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x12, 0x65, 0x0a,
+	0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x12, 0x51, 0x0a,
 	0x0e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3e, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72,
-	0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73,
-	0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69,
-	0x6c, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0d, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c,
-	0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x03, 0x20,
-	0x01, 0x28, 0x05, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65,
-	0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67,
-	0x68, 0x74, 0x12, 0x50, 0x0a, 0x0d, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x5f, 0x73, 0x6f, 0x75,
-	0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x63, 0x69, 0x73,
-	0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e,
-	0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x57, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f,
-	0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f,
-	0x75, 0x72, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x0a, 0x63, 0x73, 0x33, 0x5f, 0x73, 0x6f, 0x75, 0x72,
-	0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e,
-	0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61,
-	0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x43, 0x53, 0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
-	0x48, 0x00, 0x52, 0x09, 0x63, 0x73, 0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x21, 0x0a,
-	0x0d, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07,
-	0x0a, 0x03, 0x50, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4a, 0x50, 0x47, 0x10, 0x01,
-	0x42, 0x08, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x50, 0x0a, 0x14, 0x47, 0x65,
-	0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c,
-	0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70, 0x65, 0x32, 0x87, 0x01, 0x0a,
-	0x10, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
-	0x65, 0x12, 0x73, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69,
-	0x6c, 0x12, 0x30, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
-	0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e,
-	0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69,
-	0x63, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76,
-	0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xeb, 0x02, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75,
-	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f,
-	0x63, 0x69, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e,
-	0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x74,
-	0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x76, 0x30, 0x92, 0x41, 0xa4, 0x02,
-	0x12, 0xb8, 0x01, 0x0a, 0x22, 0x6f, 0x77, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x49, 0x6e,
-	0x66, 0x69, 0x6e, 0x69, 0x74, 0x65, 0x20, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x20, 0x74, 0x68, 0x75,
-	0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x47, 0x0a, 0x0d, 0x6f, 0x77, 0x6e, 0x43, 0x6c,
-	0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62, 0x48, 0x12, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a,
-	0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e,
-	0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x1a, 0x14, 0x73, 0x75, 0x70, 0x70,
-	0x6f, 0x72, 0x74, 0x40, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x63, 0x6f, 0x6d,
-	0x2a, 0x42, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, 0x30, 0x12, 0x34,
-	0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
-	0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73,
-	0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 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, 0x3f, 0x0a, 0x10, 0x44, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72,
-	0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x12, 0x2b, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f,
-	0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65, 0x78,
-	0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61,
-	0x69, 0x6c, 0x73, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73,
+	0x2e, 0x76, 0x30, 0x2e, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x54, 0x79, 0x70,
+	0x65, 0x52, 0x0d, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x54, 0x79, 0x70, 0x65,
+	0x12, 0x14, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52,
+	0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x50,
+	0x0a, 0x0d, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73,
+	0x2e, 0x76, 0x30, 0x2e, 0x57, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
+	0x48, 0x00, 0x52, 0x0c, 0x77, 0x65, 0x62, 0x64, 0x61, 0x76, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
+	0x12, 0x47, 0x0a, 0x0a, 0x63, 0x73, 0x33, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x06,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e,
+	0x76, 0x30, 0x2e, 0x43, 0x53, 0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x09,
+	0x63, 0x73, 0x33, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x73, 0x6f, 0x75,
+	0x72, 0x63, 0x65, 0x22, 0x7e, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e,
+	0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x64,
+	0x61, 0x74, 0x61, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
+	0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b,
+	0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66,
+	0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74,
+	0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x74,
+	0x79, 0x70, 0x65, 0x32, 0x87, 0x01, 0x0a, 0x10, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69,
+	0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x54,
+	0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x12, 0x30, 0x2e, 0x6f, 0x63, 0x69, 0x73, 0x2e,
+	0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61,
+	0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62, 0x6e,
+	0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x6f, 0x63, 0x69,
+	0x73, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x74, 0x68, 0x75, 0x6d, 0x62,
+	0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x68, 0x75, 0x6d,
+	0x62, 0x6e, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xeb, 0x02,
+	0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e,
+	0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x73, 0x65, 0x72,
+	0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73,
+	0x2f, 0x76, 0x30, 0x92, 0x41, 0xa4, 0x02, 0x12, 0xb8, 0x01, 0x0a, 0x22, 0x6f, 0x77, 0x6e, 0x43,
+	0x6c, 0x6f, 0x75, 0x64, 0x20, 0x49, 0x6e, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x65, 0x20, 0x53, 0x63,
+	0x61, 0x6c, 0x65, 0x20, 0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x22, 0x47,
+	0x0a, 0x0d, 0x6f, 0x77, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62, 0x48, 0x12,
+	0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
+	0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69,
+	0x73, 0x1a, 0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x77, 0x6e, 0x63, 0x6c,
+	0x6f, 0x75, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0x2a, 0x42, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68,
+	0x65, 0x2d, 0x32, 0x2e, 0x30, 0x12, 0x34, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67,
+	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f,
+	0x75, 0x64, 0x2f, 0x6f, 0x63, 0x69, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73,
+	0x74, 0x65, 0x72, 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, 0x3f, 0x0a, 0x10, 0x44, 0x65,
+	0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x12, 0x2b,
+	0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x6f, 0x77, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
+	0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f,
+	0x74, 0x68, 0x75, 0x6d, 0x62, 0x6e, 0x61, 0x69, 0x6c, 0x73, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
@@ -334,21 +296,20 @@ func file_ocis_services_thumbnails_v0_thumbnails_proto_rawDescGZIP() []byte {
 	return file_ocis_services_thumbnails_v0_thumbnails_proto_rawDescData
 }
 
-var file_ocis_services_thumbnails_v0_thumbnails_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
 var file_ocis_services_thumbnails_v0_thumbnails_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
 var file_ocis_services_thumbnails_v0_thumbnails_proto_goTypes = []interface{}{
-	(GetThumbnailRequest_ThumbnailType)(0), // 0: ocis.services.thumbnails.v0.GetThumbnailRequest.ThumbnailType
-	(*GetThumbnailRequest)(nil),            // 1: ocis.services.thumbnails.v0.GetThumbnailRequest
-	(*GetThumbnailResponse)(nil),           // 2: ocis.services.thumbnails.v0.GetThumbnailResponse
-	(*v0.WebdavSource)(nil),                // 3: ocis.messages.thumbnails.v0.WebdavSource
-	(*v0.CS3Source)(nil),                   // 4: ocis.messages.thumbnails.v0.CS3Source
+	(*GetThumbnailRequest)(nil),  // 0: ocis.services.thumbnails.v0.GetThumbnailRequest
+	(*GetThumbnailResponse)(nil), // 1: ocis.services.thumbnails.v0.GetThumbnailResponse
+	(v0.ThumbnailType)(0),        // 2: ocis.messages.thumbnails.v0.ThumbnailType
+	(*v0.WebdavSource)(nil),      // 3: ocis.messages.thumbnails.v0.WebdavSource
+	(*v0.CS3Source)(nil),         // 4: ocis.messages.thumbnails.v0.CS3Source
 }
 var file_ocis_services_thumbnails_v0_thumbnails_proto_depIdxs = []int32{
-	0, // 0: ocis.services.thumbnails.v0.GetThumbnailRequest.thumbnail_type:type_name -> ocis.services.thumbnails.v0.GetThumbnailRequest.ThumbnailType
+	2, // 0: ocis.services.thumbnails.v0.GetThumbnailRequest.thumbnail_type:type_name -> ocis.messages.thumbnails.v0.ThumbnailType
 	3, // 1: ocis.services.thumbnails.v0.GetThumbnailRequest.webdav_source:type_name -> ocis.messages.thumbnails.v0.WebdavSource
 	4, // 2: ocis.services.thumbnails.v0.GetThumbnailRequest.cs3_source:type_name -> ocis.messages.thumbnails.v0.CS3Source
-	1, // 3: ocis.services.thumbnails.v0.ThumbnailService.GetThumbnail:input_type -> ocis.services.thumbnails.v0.GetThumbnailRequest
-	2, // 4: ocis.services.thumbnails.v0.ThumbnailService.GetThumbnail:output_type -> ocis.services.thumbnails.v0.GetThumbnailResponse
+	0, // 3: ocis.services.thumbnails.v0.ThumbnailService.GetThumbnail:input_type -> ocis.services.thumbnails.v0.GetThumbnailRequest
+	1, // 4: ocis.services.thumbnails.v0.ThumbnailService.GetThumbnail:output_type -> ocis.services.thumbnails.v0.GetThumbnailResponse
 	4, // [4:5] is the sub-list for method output_type
 	3, // [3:4] is the sub-list for method input_type
 	3, // [3:3] is the sub-list for extension type_name
@@ -396,14 +357,13 @@ func file_ocis_services_thumbnails_v0_thumbnails_proto_init() {
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_ocis_services_thumbnails_v0_thumbnails_proto_rawDesc,
-			NumEnums:      1,
+			NumEnums:      0,
 			NumMessages:   2,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
 		GoTypes:           file_ocis_services_thumbnails_v0_thumbnails_proto_goTypes,
 		DependencyIndexes: file_ocis_services_thumbnails_v0_thumbnails_proto_depIdxs,
-		EnumInfos:         file_ocis_services_thumbnails_v0_thumbnails_proto_enumTypes,
 		MessageInfos:      file_ocis_services_thumbnails_v0_thumbnails_proto_msgTypes,
 	}.Build()
 	File_ocis_services_thumbnails_v0_thumbnails_proto = out.File
diff --git a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.swagger.json b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.swagger.json
index f79a4f11908..7e4731e46d7 100644
--- a/protogen/gen/ocis/services/thumbnails/v0/thumbnails.swagger.json
+++ b/protogen/gen/ocis/services/thumbnails/v0/thumbnails.swagger.json
@@ -30,15 +30,6 @@
   ],
   "paths": {},
   "definitions": {
-    "GetThumbnailRequestThumbnailType": {
-      "type": "string",
-      "enum": [
-        "PNG",
-        "JPG"
-      ],
-      "default": "PNG",
-      "description": "The file types to which the thumbnail can get encoded to."
-    },
     "protobufAny": {
       "type": "object",
       "properties": {
@@ -80,10 +71,13 @@
     "v0GetThumbnailResponse": {
       "type": "object",
       "properties": {
-        "thumbnail": {
+        "dataEndpoint": {
+          "type": "string",
+          "description": "The endpoint where the thumbnail can be downloaded."
+        },
+        "transferToken": {
           "type": "string",
-          "format": "byte",
-          "title": "The thumbnail as a binary"
+          "description": "The transfer token to be able to download the thumbnail."
         },
         "mimetype": {
           "type": "string",
@@ -92,6 +86,16 @@
       },
       "title": "The service response"
     },
+    "v0ThumbnailType": {
+      "type": "string",
+      "enum": [
+        "PNG",
+        "JPG",
+        "GIF"
+      ],
+      "default": "PNG",
+      "description": "The file types to which the thumbnail can be encoded to."
+    },
     "v0WebdavSource": {
       "type": "object",
       "properties": {
diff --git a/protogen/proto/ocis/messages/thumbnails/v0/thumbnails.proto b/protogen/proto/ocis/messages/thumbnails/v0/thumbnails.proto
index 3964970ef87..d5c0d8a77bd 100644
--- a/protogen/proto/ocis/messages/thumbnails/v0/thumbnails.proto
+++ b/protogen/proto/ocis/messages/thumbnails/v0/thumbnails.proto
@@ -21,3 +21,10 @@ message CS3Source {
     string path = 1;
     string authorization = 2;
 }
+
+// The file types to which the thumbnail can be encoded to.
+enum ThumbnailType {
+        PNG = 0; // Represents PNG type
+        JPG = 1; // Represents JPG type
+        GIF = 2; // Represents GIF type
+}
\ No newline at end of file
diff --git a/protogen/proto/ocis/services/thumbnails/v0/thumbnails.proto b/protogen/proto/ocis/services/thumbnails/v0/thumbnails.proto
index 0b7eae51715..148175586d2 100644
--- a/protogen/proto/ocis/services/thumbnails/v0/thumbnails.proto
+++ b/protogen/proto/ocis/services/thumbnails/v0/thumbnails.proto
@@ -41,13 +41,8 @@ service ThumbnailService {
 message GetThumbnailRequest {
     // The path to the source image
     string filepath = 1;
-    // The file types to which the thumbnail can get encoded to.
-    enum ThumbnailType {
-        PNG = 0; // Represents PNG type
-        JPG = 1; // Represents JPG type
-    }
     // The type to which the thumbnail should get encoded to.
-    ThumbnailType thumbnail_type = 2;
+    ocis.messages.thumbnails.v0.ThumbnailType thumbnail_type = 2;
     // The width of the thumbnail
     int32 width = 3;
     // The height of the thumbnail
@@ -60,8 +55,10 @@ message GetThumbnailRequest {
 
 // The service response
 message GetThumbnailResponse {
-    // The thumbnail as a binary
-    bytes thumbnail = 1;
+    // The endpoint where the thumbnail can be downloaded.
+    string data_endpoint = 1;
+    // The transfer token to be able to download the thumbnail.
+    string transfer_token = 2;
     // The mimetype of the thumbnail
-    string mimetype = 2;
+    string mimetype = 3;
 }
diff --git a/thumbnails/pkg/command/server.go b/thumbnails/pkg/command/server.go
index f44b230d36c..86fb9d91a79 100644
--- a/thumbnails/pkg/command/server.go
+++ b/thumbnails/pkg/command/server.go
@@ -12,6 +12,7 @@ import (
 	"github.com/owncloud/ocis/thumbnails/pkg/metrics"
 	"github.com/owncloud/ocis/thumbnails/pkg/server/debug"
 	"github.com/owncloud/ocis/thumbnails/pkg/server/grpc"
+	"github.com/owncloud/ocis/thumbnails/pkg/server/http"
 	"github.com/owncloud/ocis/thumbnails/pkg/tracing"
 	"github.com/urfave/cli/v2"
 )
@@ -57,9 +58,7 @@ func Server(cfg *config.Config) *cli.Command {
 				grpc.Metrics(metrics),
 			)
 
-			gr.Add(func() error {
-				return service.Run()
-			}, func(_ error) {
+			gr.Add(service.Run, func(_ error) {
 				fmt.Println("shutting down grpc server")
 				cancel()
 			})
@@ -79,6 +78,28 @@ func Server(cfg *config.Config) *cli.Command {
 				cancel()
 			})
 
+			httpServer, err := http.Server(
+				http.Logger(logger),
+				http.Context(ctx),
+				http.Config(cfg),
+				http.Metrics(metrics),
+				http.Namespace(cfg.HTTP.Namespace),
+			)
+
+			if err != nil {
+				logger.Info().
+					Err(err).
+					Str("transport", "http").
+					Msg("Failed to initialize server")
+
+				return err
+			}
+
+			gr.Add(httpServer.Run, func(_ error) {
+				logger.Info().Str("server", "http").Msg("shutting down server")
+				cancel()
+			})
+
 			return gr.Run()
 		},
 	}
diff --git a/thumbnails/pkg/config/config.go b/thumbnails/pkg/config/config.go
index 527f94a50e7..2311a4e4dd0 100644
--- a/thumbnails/pkg/config/config.go
+++ b/thumbnails/pkg/config/config.go
@@ -17,6 +17,7 @@ type Config struct {
 	Debug   Debug    `ocisConfig:"debug"`
 
 	GRPC GRPC `ocisConfig:"grpc"`
+	HTTP HTTP `ocisConfig:"http"`
 
 	Thumbnail Thumbnail `ocisConfig:"thumbnail"`
 
@@ -41,4 +42,6 @@ type Thumbnail struct {
 	CS3AllowInsecure    bool              `ocisConfig:"cs3_allow_insecure" env:"OCIS_INSECURE;THUMBNAILS_CS3SOURCE_INSECURE"`
 	RevaGateway         string            `ocisConfig:"reva_gateway" env:"REVA_GATEWAY"` //TODO: use REVA config
 	FontMapFile         string            `ocisConfig:"font_map_file" env:"THUMBNAILS_TXT_FONTMAP_FILE"`
+	TransferTokenSecret string            `ocisConfig:"transfer_token" env:"THUMBNAILS_TRANSFER_TOKEN"`
+	DataEndpoint        string            `ocisConfig:"data_endpoint" env:"THUMBNAILS_DATA_ENDPOINT"`
 }
diff --git a/thumbnails/pkg/config/defaultconfig.go b/thumbnails/pkg/config/defaultconfig.go
index 8f59db53e3d..5e4a3e42323 100644
--- a/thumbnails/pkg/config/defaultconfig.go
+++ b/thumbnails/pkg/config/defaultconfig.go
@@ -18,6 +18,11 @@ func DefaultConfig() *Config {
 			Addr:      "127.0.0.1:9185",
 			Namespace: "com.owncloud.api",
 		},
+		HTTP: HTTP{
+			Addr:      "127.0.0.1:9186",
+			Root:      "/thumbnails",
+			Namespace: "com.owncloud.web",
+		},
 		Service: Service{
 			Name: "thumbnails",
 		},
@@ -29,6 +34,8 @@ func DefaultConfig() *Config {
 			WebdavAllowInsecure: true,
 			RevaGateway:         "127.0.0.1:9142",
 			CS3AllowInsecure:    false,
+			TransferTokenSecret: "changemeplease",
+			DataEndpoint:        "http://127.0.0.1:9186/thumbnails/data",
 		},
 	}
 }
diff --git a/thumbnails/pkg/config/http.go b/thumbnails/pkg/config/http.go
new file mode 100644
index 00000000000..f3b4d572845
--- /dev/null
+++ b/thumbnails/pkg/config/http.go
@@ -0,0 +1,8 @@
+package config
+
+// HTTP defines the available http configuration.
+type HTTP struct {
+	Addr      string `ocisConfig:"addr" env:"THUMBNAILS_HTTP_ADDR"`
+	Root      string `ocisConfig:"root" env:"THUMBNAILS_HTTP_ROOT"`
+	Namespace string
+}
diff --git a/thumbnails/pkg/preprocessor/preprocessor.go b/thumbnails/pkg/preprocessor/preprocessor.go
index f521967d57b..d0b1acc6f93 100644
--- a/thumbnails/pkg/preprocessor/preprocessor.go
+++ b/thumbnails/pkg/preprocessor/preprocessor.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"image"
 	"image/draw"
+	"image/gif"
 	"io"
 	"math"
 	"mime"
@@ -16,12 +17,12 @@ import (
 )
 
 type FileConverter interface {
-	Convert(r io.Reader) (image.Image, error)
+	Convert(r io.Reader) (interface{}, error)
 }
 
 type ImageDecoder struct{}
 
-func (i ImageDecoder) Convert(r io.Reader) (image.Image, error) {
+func (i ImageDecoder) Convert(r io.Reader) (interface{}, error) {
 	img, _, err := image.Decode(r)
 	if err != nil {
 		return nil, errors.Wrap(err, `could not decode the image`)
@@ -29,11 +30,21 @@ func (i ImageDecoder) Convert(r io.Reader) (image.Image, error) {
 	return img, nil
 }
 
+type GifDecoder struct{}
+
+func (i GifDecoder) Convert(r io.Reader) (interface{}, error) {
+	img, err := gif.DecodeAll(r)
+	if err != nil {
+		return nil, errors.Wrap(err, `could not decode the image`)
+	}
+	return img, nil
+}
+
 type TxtToImageConverter struct {
 	fontLoader *FontLoader
 }
 
-func (t TxtToImageConverter) Convert(r io.Reader) (image.Image, error) {
+func (t TxtToImageConverter) Convert(r io.Reader) (interface{}, error) {
 	img := image.NewRGBA(image.Rect(0, 0, 640, 480))
 
 	imgBounds := img.Bounds()
@@ -183,6 +194,8 @@ func ForType(mimeType string, opts map[string]interface{}) FileConverter {
 		return TxtToImageConverter{
 			fontLoader: fontLoader,
 		}
+	case "image/gif":
+		return GifDecoder{}
 	default:
 		return ImageDecoder{}
 	}
diff --git a/thumbnails/pkg/server/grpc/server.go b/thumbnails/pkg/server/grpc/server.go
index 825d8088149..f4e11e451bc 100644
--- a/thumbnails/pkg/server/grpc/server.go
+++ b/thumbnails/pkg/server/grpc/server.go
@@ -5,8 +5,8 @@ import (
 	"github.com/owncloud/ocis/ocis-pkg/service/grpc"
 	"github.com/owncloud/ocis/ocis-pkg/version"
 	thumbnailssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/thumbnails/v0"
-	svc "github.com/owncloud/ocis/thumbnails/pkg/service/v0"
-	"github.com/owncloud/ocis/thumbnails/pkg/service/v0/decorators"
+	svc "github.com/owncloud/ocis/thumbnails/pkg/service/grpc/v0"
+	"github.com/owncloud/ocis/thumbnails/pkg/service/grpc/v0/decorators"
 	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/imgsource"
 	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
 )
diff --git a/thumbnails/pkg/server/http/option.go b/thumbnails/pkg/server/http/option.go
new file mode 100644
index 00000000000..9f1fa828e23
--- /dev/null
+++ b/thumbnails/pkg/server/http/option.go
@@ -0,0 +1,69 @@
+package http
+
+import (
+	"context"
+
+	"github.com/owncloud/ocis/ocis-pkg/log"
+	"github.com/owncloud/ocis/thumbnails/pkg/config"
+	"github.com/owncloud/ocis/thumbnails/pkg/metrics"
+	"github.com/urfave/cli/v2"
+)
+
+// Option defines a single option function.
+type Option func(o *Options)
+
+// Options defines the available options for this package.
+type Options struct {
+	Namespace string
+	Logger    log.Logger
+	Context   context.Context
+	Config    *config.Config
+	Metrics   *metrics.Metrics
+	Flags     []cli.Flag
+}
+
+// newOptions initializes the available default options.
+func newOptions(opts ...Option) Options {
+	opt := Options{}
+
+	for _, o := range opts {
+		o(&opt)
+	}
+
+	return opt
+}
+
+// Logger provides a function to set the logger option.
+func Logger(val log.Logger) Option {
+	return func(o *Options) {
+		o.Logger = val
+	}
+}
+
+// Context provides a function to set the context option.
+func Context(val context.Context) Option {
+	return func(o *Options) {
+		o.Context = val
+	}
+}
+
+// Config provides a function to set the config option.
+func Config(val *config.Config) Option {
+	return func(o *Options) {
+		o.Config = val
+	}
+}
+
+// Metrics provides a function to set the metrics option.
+func Metrics(val *metrics.Metrics) Option {
+	return func(o *Options) {
+		o.Metrics = val
+	}
+}
+
+// Namespace provides a function to set the Namespace option.
+func Namespace(val string) Option {
+	return func(o *Options) {
+		o.Namespace = val
+	}
+}
diff --git a/thumbnails/pkg/server/http/server.go b/thumbnails/pkg/server/http/server.go
new file mode 100644
index 00000000000..10d013a3498
--- /dev/null
+++ b/thumbnails/pkg/server/http/server.go
@@ -0,0 +1,58 @@
+package http
+
+import (
+	"github.com/go-chi/chi/v5/middleware"
+	ocismiddleware "github.com/owncloud/ocis/ocis-pkg/middleware"
+	"github.com/owncloud/ocis/ocis-pkg/service/http"
+	"github.com/owncloud/ocis/ocis-pkg/version"
+	svc "github.com/owncloud/ocis/thumbnails/pkg/service/http/v0"
+	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
+	"go-micro.dev/v4"
+)
+
+// Server initializes the http service and server.
+func Server(opts ...Option) (http.Service, error) {
+	options := newOptions(opts...)
+
+	service := http.NewService(
+		http.Logger(options.Logger),
+		http.Name(options.Config.Service.Name),
+		http.Version(version.String),
+		http.Namespace(options.Config.HTTP.Namespace),
+		http.Address(options.Config.HTTP.Addr),
+		http.Context(options.Context),
+	)
+
+	handle := svc.NewService(
+		svc.Logger(options.Logger),
+		svc.Config(options.Config),
+		svc.Middleware(
+			middleware.RealIP,
+			middleware.RequestID,
+			// ocismiddleware.Secure,
+			ocismiddleware.Version(
+				options.Config.Service.Name,
+				version.String,
+			),
+			ocismiddleware.Logger(options.Logger),
+		),
+		svc.ThumbnailStorage(
+			storage.NewFileSystemStorage(
+				options.Config.Thumbnail.FileSystemStorage,
+				options.Logger,
+			),
+		),
+	)
+
+	{
+		handle = svc.NewInstrument(handle, options.Metrics)
+		handle = svc.NewLogging(handle, options.Logger)
+		handle = svc.NewTracing(handle)
+	}
+
+	if err := micro.RegisterHandler(service.Server(), handle); err != nil {
+		return http.Service{}, err
+	}
+
+	return service, nil
+}
diff --git a/thumbnails/pkg/service/v0/decorators/base.go b/thumbnails/pkg/service/grpc/v0/decorators/base.go
similarity index 100%
rename from thumbnails/pkg/service/v0/decorators/base.go
rename to thumbnails/pkg/service/grpc/v0/decorators/base.go
diff --git a/thumbnails/pkg/service/v0/decorators/instrument.go b/thumbnails/pkg/service/grpc/v0/decorators/instrument.go
similarity index 100%
rename from thumbnails/pkg/service/v0/decorators/instrument.go
rename to thumbnails/pkg/service/grpc/v0/decorators/instrument.go
diff --git a/thumbnails/pkg/service/v0/decorators/logging.go b/thumbnails/pkg/service/grpc/v0/decorators/logging.go
similarity index 100%
rename from thumbnails/pkg/service/v0/decorators/logging.go
rename to thumbnails/pkg/service/grpc/v0/decorators/logging.go
diff --git a/thumbnails/pkg/service/v0/decorators/tracing.go b/thumbnails/pkg/service/grpc/v0/decorators/tracing.go
similarity index 100%
rename from thumbnails/pkg/service/v0/decorators/tracing.go
rename to thumbnails/pkg/service/grpc/v0/decorators/tracing.go
diff --git a/thumbnails/pkg/service/v0/option.go b/thumbnails/pkg/service/grpc/v0/option.go
similarity index 100%
rename from thumbnails/pkg/service/v0/option.go
rename to thumbnails/pkg/service/grpc/v0/option.go
diff --git a/thumbnails/pkg/service/v0/service.go b/thumbnails/pkg/service/grpc/v0/service.go
similarity index 66%
rename from thumbnails/pkg/service/v0/service.go
rename to thumbnails/pkg/service/grpc/v0/service.go
index 87c638f2255..ef1b16b87a2 100644
--- a/thumbnails/pkg/service/v0/service.go
+++ b/thumbnails/pkg/service/grpc/v0/service.go
@@ -6,15 +6,19 @@ import (
 	"net/url"
 	"path"
 	"strings"
+	"time"
 
 	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
 	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
 	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
 	revactx "github.com/cs3org/reva/v2/pkg/ctx"
+	"github.com/golang-jwt/jwt/v4"
 	"github.com/owncloud/ocis/ocis-pkg/log"
+	thumbnailsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/thumbnails/v0"
 	thumbnailssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/thumbnails/v0"
 	"github.com/owncloud/ocis/thumbnails/pkg/preprocessor"
-	"github.com/owncloud/ocis/thumbnails/pkg/service/v0/decorators"
+	"github.com/owncloud/ocis/thumbnails/pkg/service/grpc/v0/decorators"
+	tjwt "github.com/owncloud/ocis/thumbnails/pkg/service/jwt"
 	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail"
 	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/imgsource"
 	"github.com/pkg/errors"
@@ -44,6 +48,8 @@ func NewService(opts ...Option) decorators.DecoratedService {
 		preprocessorOpts: PreprocessorOpts{
 			TxtFontFileMap: options.Config.Thumbnail.FontMapFile,
 		},
+		dataEndpoint:        options.Config.Thumbnail.DataEndpoint,
+		transferTokenSecret: options.Config.Thumbnail.TransferTokenSecret,
 	}
 
 	return svc
@@ -51,13 +57,15 @@ func NewService(opts ...Option) decorators.DecoratedService {
 
 // Thumbnail implements the GRPC handler.
 type Thumbnail struct {
-	serviceID        string
-	manager          thumbnail.Manager
-	webdavSource     imgsource.Source
-	cs3Source        imgsource.Source
-	logger           log.Logger
-	cs3Client        gateway.GatewayAPIClient
-	preprocessorOpts PreprocessorOpts
+	serviceID           string
+	dataEndpoint        string
+	transferTokenSecret string
+	manager             thumbnail.Manager
+	webdavSource        imgsource.Source
+	cs3Source           imgsource.Source
+	logger              log.Logger
+	cs3Client           gateway.GatewayAPIClient
+	preprocessorOpts    PreprocessorOpts
 }
 
 type PreprocessorOpts struct {
@@ -66,24 +74,28 @@ type PreprocessorOpts struct {
 
 // GetThumbnail retrieves a thumbnail for an image
 func (g Thumbnail) GetThumbnail(ctx context.Context, req *thumbnailssvc.GetThumbnailRequest, rsp *thumbnailssvc.GetThumbnailResponse) error {
-	_, ok := thumbnailssvc.GetThumbnailRequest_ThumbnailType_value[req.ThumbnailType.String()]
+	tType, ok := thumbnailsmsg.ThumbnailType_name[int32(req.ThumbnailType)]
 	if !ok {
-		g.logger.Debug().Str("thumbnail_type", req.ThumbnailType.String()).Msg("unsupported thumbnail type")
+		g.logger.Debug().Str("thumbnail_type", tType).Msg("unsupported thumbnail type")
 		return nil
 	}
-	encoder := thumbnail.EncoderForType(req.ThumbnailType.String())
-	if encoder == nil {
-		g.logger.Debug().Str("thumbnail_type", req.ThumbnailType.String()).Msg("unsupported thumbnail type")
+	generator, err := thumbnail.GeneratorForType(tType)
+	if err != nil {
+		g.logger.Debug().Str("thumbnail_type", tType).Msg("unsupported thumbnail type")
+		return nil
+	}
+	encoder, err := thumbnail.EncoderForType(tType)
+	if err != nil {
+		g.logger.Debug().Str("thumbnail_type", tType).Msg("unsupported thumbnail type")
 		return nil
 	}
 
-	var thumb []byte
-	var err error
+	var key string
 	switch {
 	case req.GetWebdavSource() != nil:
-		thumb, err = g.handleWebdavSource(ctx, req, encoder)
+		key, err = g.handleWebdavSource(ctx, req, generator, encoder)
 	case req.GetCs3Source() != nil:
-		thumb, err = g.handleCS3Source(ctx, req, encoder)
+		key, err = g.handleCS3Source(ctx, req, generator, encoder)
 	default:
 		g.logger.Error().Msg("no image source provided")
 		return merrors.BadRequest(g.serviceID, "image source is missing")
@@ -92,33 +104,53 @@ func (g Thumbnail) GetThumbnail(ctx context.Context, req *thumbnailssvc.GetThumb
 		return err
 	}
 
-	rsp.Thumbnail = thumb
+	claims := tjwt.ThumbnailClaims{
+		Key: key,
+		RegisteredClaims: jwt.RegisteredClaims{
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Minute)),
+		},
+	}
+
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	transferToken, err := token.SignedString([]byte(g.transferTokenSecret))
+	if err != nil {
+		g.logger.Error().
+			Err(err).
+			Msg("GetThumbnail: failed to sign token")
+		return merrors.InternalServerError(g.serviceID, "couldn't finish request")
+	}
+	rsp.DataEndpoint = g.dataEndpoint
+	rsp.TransferToken = transferToken
 	rsp.Mimetype = encoder.MimeType()
 	return nil
 }
 
-func (g Thumbnail) handleCS3Source(ctx context.Context, req *thumbnailssvc.GetThumbnailRequest, encoder thumbnail.Encoder) ([]byte, error) {
+func (g Thumbnail) handleCS3Source(ctx context.Context,
+	req *thumbnailssvc.GetThumbnailRequest,
+	generator thumbnail.Generator,
+	encoder thumbnail.Encoder) (string, error) {
 	src := req.GetCs3Source()
 	sRes, err := g.stat(src.Path, src.Authorization)
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 
 	tr := thumbnail.Request{
 		Resolution: image.Rect(0, 0, int(req.Width), int(req.Height)),
+		Generator:  generator,
 		Encoder:    encoder,
 		Checksum:   sRes.GetInfo().GetChecksum().GetSum(),
 	}
 
-	thumb, ok := g.manager.Get(tr)
-	if ok {
-		return thumb, nil
+	if key, exists := g.manager.CheckThumbnail(tr); exists {
+		return key, nil
 	}
 
 	ctx = imgsource.ContextSetAuthorization(ctx, src.Authorization)
 	r, err := g.cs3Source.Get(ctx, src.Path)
 	if err != nil {
-		return nil, merrors.InternalServerError(g.serviceID, "could not get image from source: %s", err.Error())
+		return "", merrors.InternalServerError(g.serviceID, "could not get image from source: %s", err.Error())
 	}
 	defer r.Close() // nolint:errcheck
 	ppOpts := map[string]interface{}{
@@ -127,20 +159,24 @@ func (g Thumbnail) handleCS3Source(ctx context.Context, req *thumbnailssvc.GetTh
 	pp := preprocessor.ForType(sRes.GetInfo().GetMimeType(), ppOpts)
 	img, err := pp.Convert(r)
 	if img == nil || err != nil {
-		return nil, merrors.InternalServerError(g.serviceID, "could not get image")
-	}
-	if thumb, err = g.manager.Generate(tr, img); err != nil {
-		return nil, err
+		return "", merrors.InternalServerError(g.serviceID, "could not get image")
 	}
 
-	return thumb, nil
+	key, err := g.manager.Generate(tr, img)
+	if err != nil {
+		return "", err
+	}
+	return key, nil
 }
 
-func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.GetThumbnailRequest, encoder thumbnail.Encoder) ([]byte, error) {
+func (g Thumbnail) handleWebdavSource(ctx context.Context,
+	req *thumbnailssvc.GetThumbnailRequest,
+	generator thumbnail.Generator,
+	encoder thumbnail.Encoder) (string, error) {
 	src := req.GetWebdavSource()
 	imgURL, err := url.Parse(src.Url)
 	if err != nil {
-		return nil, errors.Wrap(err, "source url is invalid")
+		return "", errors.Wrap(err, "source url is invalid")
 	}
 
 	var auth, statPath string
@@ -168,7 +204,7 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge
 		}
 
 		if err != nil {
-			return nil, merrors.InternalServerError(g.serviceID, "could not authenticate: %s", err.Error())
+			return "", merrors.InternalServerError(g.serviceID, "could not authenticate: %s", err.Error())
 		}
 		auth = rsp.Token
 		statPath = path.Join("/public", src.PublicLinkToken, req.Filepath)
@@ -178,16 +214,17 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge
 	}
 	sRes, err := g.stat(statPath, auth)
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 	tr := thumbnail.Request{
 		Resolution: image.Rect(0, 0, int(req.Width), int(req.Height)),
+		Generator:  generator,
 		Encoder:    encoder,
 		Checksum:   sRes.GetInfo().GetChecksum().GetSum(),
 	}
-	thumb, ok := g.manager.Get(tr)
-	if ok {
-		return thumb, nil
+
+	if key, exists := g.manager.CheckThumbnail(tr); exists {
+		return key, nil
 	}
 
 	if src.WebdavAuthorization != "" {
@@ -196,7 +233,7 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge
 	imgURL.RawQuery = ""
 	r, err := g.webdavSource.Get(ctx, imgURL.String())
 	if err != nil {
-		return nil, merrors.InternalServerError(g.serviceID, "could not get image from source: %s", err.Error())
+		return "", merrors.InternalServerError(g.serviceID, "could not get image from source: %s", err.Error())
 	}
 	defer r.Close() // nolint:errcheck
 	ppOpts := map[string]interface{}{
@@ -205,13 +242,14 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge
 	pp := preprocessor.ForType(sRes.GetInfo().GetMimeType(), ppOpts)
 	img, err := pp.Convert(r)
 	if img == nil || err != nil {
-		return nil, merrors.InternalServerError(g.serviceID, "could not get image")
-	}
-	if thumb, err = g.manager.Generate(tr, img); err != nil {
-		return nil, err
+		return "", merrors.InternalServerError(g.serviceID, "could not get image")
 	}
 
-	return thumb, nil
+	key, err := g.manager.Generate(tr, img)
+	if err != nil {
+		return "", err
+	}
+	return key, nil
 }
 
 func (g Thumbnail) stat(path, auth string) (*provider.StatResponse, error) {
diff --git a/thumbnails/pkg/service/http/v0/instrument.go b/thumbnails/pkg/service/http/v0/instrument.go
new file mode 100644
index 00000000000..baab1c6fee8
--- /dev/null
+++ b/thumbnails/pkg/service/http/v0/instrument.go
@@ -0,0 +1,30 @@
+package svc
+
+import (
+	"net/http"
+
+	"github.com/owncloud/ocis/thumbnails/pkg/metrics"
+)
+
+// NewInstrument returns a service that instruments metrics.
+func NewInstrument(next Service, metrics *metrics.Metrics) Service {
+	return instrument{
+		next:    next,
+		metrics: metrics,
+	}
+}
+
+type instrument struct {
+	next    Service
+	metrics *metrics.Metrics
+}
+
+// ServeHTTP implements the Service interface.
+func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	i.next.ServeHTTP(w, r)
+}
+
+// GetThumbnail implements the Service interface.
+func (i instrument) GetThumbnail(w http.ResponseWriter, r *http.Request) {
+	i.next.GetThumbnail(w, r)
+}
diff --git a/thumbnails/pkg/service/http/v0/logging.go b/thumbnails/pkg/service/http/v0/logging.go
new file mode 100644
index 00000000000..84701b1b072
--- /dev/null
+++ b/thumbnails/pkg/service/http/v0/logging.go
@@ -0,0 +1,30 @@
+package svc
+
+import (
+	"net/http"
+
+	"github.com/owncloud/ocis/ocis-pkg/log"
+)
+
+// NewLogging returns a service that logs messages.
+func NewLogging(next Service, logger log.Logger) Service {
+	return logging{
+		next:   next,
+		logger: logger,
+	}
+}
+
+type logging struct {
+	next   Service
+	logger log.Logger
+}
+
+// ServeHTTP implements the Service interface.
+func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	l.next.ServeHTTP(w, r)
+}
+
+// GetThumbnail implements the Service interface.
+func (l logging) GetThumbnail(w http.ResponseWriter, r *http.Request) {
+	l.next.GetThumbnail(w, r)
+}
diff --git a/thumbnails/pkg/service/http/v0/option.go b/thumbnails/pkg/service/http/v0/option.go
new file mode 100644
index 00000000000..efcf65f358f
--- /dev/null
+++ b/thumbnails/pkg/service/http/v0/option.go
@@ -0,0 +1,59 @@
+package svc
+
+import (
+	"net/http"
+
+	"github.com/owncloud/ocis/ocis-pkg/log"
+	"github.com/owncloud/ocis/thumbnails/pkg/config"
+	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
+)
+
+// 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
+	Middleware       []func(http.Handler) http.Handler
+	ThumbnailStorage storage.Storage
+}
+
+// newOptions initializes the available default options.
+func newOptions(opts ...Option) Options {
+	opt := Options{}
+
+	for _, o := range opts {
+		o(&opt)
+	}
+
+	return opt
+}
+
+// Logger provides a function to set the logger option.
+func Logger(val log.Logger) Option {
+	return func(o *Options) {
+		o.Logger = val
+	}
+}
+
+// Config provides a function to set the config option.
+func Config(val *config.Config) Option {
+	return func(o *Options) {
+		o.Config = val
+	}
+}
+
+// Middleware provides a function to set the middleware option.
+func Middleware(val ...func(http.Handler) http.Handler) Option {
+	return func(o *Options) {
+		o.Middleware = val
+	}
+}
+
+// ThumbnailStorage provides a function to set the ThumbnailStorage option.
+func ThumbnailStorage(storage storage.Storage) Option {
+	return func(o *Options) {
+		o.ThumbnailStorage = storage
+	}
+}
diff --git a/thumbnails/pkg/service/http/v0/service.go b/thumbnails/pkg/service/http/v0/service.go
new file mode 100644
index 00000000000..76df357a23e
--- /dev/null
+++ b/thumbnails/pkg/service/http/v0/service.go
@@ -0,0 +1,123 @@
+package svc
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/golang-jwt/jwt/v4"
+
+	"github.com/owncloud/ocis/ocis-pkg/log"
+	"github.com/owncloud/ocis/thumbnails/pkg/config"
+	tjwt "github.com/owncloud/ocis/thumbnails/pkg/service/jwt"
+	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail"
+)
+
+type contextKey string
+
+const (
+	keyContextKey contextKey = "key"
+)
+
+// Service defines the extension handlers.
+type Service interface {
+	ServeHTTP(http.ResponseWriter, *http.Request)
+	GetThumbnail(http.ResponseWriter, *http.Request)
+}
+
+// NewService returns a service implementation for Service.
+func NewService(opts ...Option) Service {
+	options := newOptions(opts...)
+
+	m := chi.NewMux()
+	m.Use(options.Middleware...)
+
+	logger := options.Logger
+	resolutions, err := thumbnail.ParseResolutions(options.Config.Thumbnail.Resolutions)
+	if err != nil {
+		logger.Fatal().Err(err).Msg("resolutions not configured correctly")
+	}
+	svc := Thumbnails{
+		config: options.Config,
+		mux:    m,
+		logger: options.Logger,
+		manager: thumbnail.NewSimpleManager(
+			resolutions,
+			options.ThumbnailStorage,
+			logger,
+		),
+	}
+
+	m.Route(options.Config.HTTP.Root, func(r chi.Router) {
+		r.Use(svc.TransferTokenValidator)
+		r.Get("/data", svc.GetThumbnail)
+	})
+
+	return svc
+}
+
+// Thumbnails implements the business logic for Service.
+type Thumbnails struct {
+	config  *config.Config
+	logger  log.Logger
+	mux     *chi.Mux
+	manager thumbnail.Manager
+}
+
+// ServeHTTP implements the Service interface.
+func (s Thumbnails) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	s.mux.ServeHTTP(w, r)
+}
+
+// GetThumbnail implements the Service interface.
+func (s Thumbnails) GetThumbnail(w http.ResponseWriter, r *http.Request) {
+	key := r.Context().Value(keyContextKey).(string)
+
+	thumbnail, err := s.manager.GetThumbnail(key)
+	if err != nil {
+		s.logger.Error().
+			Err(err).
+			Str("key", key).
+			Msg("could not get the thumbnail")
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+	w.Header().Set("Content-Length", strconv.Itoa(len(thumbnail)))
+	if _, err = w.Write(thumbnail); err != nil {
+		s.logger.Error().
+			Err(err).
+			Str("key", key).
+			Msg("could not write the thumbnail response")
+	}
+}
+
+func (s Thumbnails) TransferTokenValidator(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		tokenString := r.Header.Get("Transfer-Token")
+		token, err := jwt.ParseWithClaims(tokenString, &tjwt.ThumbnailClaims{}, func(token *jwt.Token) (interface{}, error) {
+			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+			}
+			return []byte(s.config.Thumbnail.TransferTokenSecret), nil
+		})
+		if err != nil {
+			s.logger.Error().
+				Err(err).
+				Str("transfer-token", tokenString).
+				Msg("failed to parse transfer token")
+			w.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+
+		if claims, ok := token.Claims.(*tjwt.ThumbnailClaims); ok && token.Valid {
+			ctx := context.WithValue(r.Context(), keyContextKey, claims.Key)
+			next.ServeHTTP(w, r.WithContext(ctx))
+			return
+		}
+		w.WriteHeader(http.StatusUnauthorized)
+	})
+}
diff --git a/thumbnails/pkg/service/http/v0/tracing.go b/thumbnails/pkg/service/http/v0/tracing.go
new file mode 100644
index 00000000000..ce9a01d0f03
--- /dev/null
+++ b/thumbnails/pkg/service/http/v0/tracing.go
@@ -0,0 +1,28 @@
+package svc
+
+import (
+	"net/http"
+
+	"github.com/owncloud/ocis/ocis-pkg/middleware"
+)
+
+// NewTracing returns a service that instruments traces.
+func NewTracing(next Service) Service {
+	return tracing{
+		next: next,
+	}
+}
+
+type tracing struct {
+	next Service
+}
+
+// ServeHTTP implements the Service interface.
+func (t tracing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	middleware.TraceContext(t.next).ServeHTTP(w, r)
+}
+
+// GetThumbnail implements the Service interface.
+func (t tracing) GetThumbnail(w http.ResponseWriter, r *http.Request) {
+	t.next.GetThumbnail(w, r)
+}
diff --git a/thumbnails/pkg/service/jwt/jwt.go b/thumbnails/pkg/service/jwt/jwt.go
new file mode 100644
index 00000000000..bbe46bb94d3
--- /dev/null
+++ b/thumbnails/pkg/service/jwt/jwt.go
@@ -0,0 +1,8 @@
+package jwt
+
+import "github.com/golang-jwt/jwt/v4"
+
+type ThumbnailClaims struct {
+	jwt.RegisteredClaims
+	Key string `json:"key"`
+}
diff --git a/thumbnails/pkg/thumbnail/encoding.go b/thumbnails/pkg/thumbnail/encoding.go
index b253ca3b5fb..d9c43d2eceb 100644
--- a/thumbnails/pkg/thumbnail/encoding.go
+++ b/thumbnails/pkg/thumbnail/encoding.go
@@ -1,17 +1,33 @@
 package thumbnail
 
 import (
+	"errors"
 	"image"
+	"image/gif"
 	"image/jpeg"
 	"image/png"
 	"io"
 	"strings"
 )
 
+const (
+	typePng  = "png"
+	typeJpg  = "jpg"
+	typeJpeg = "jpeg"
+	typeGif  = "gif"
+)
+
+var (
+	// ErrInvalidType represents the error when a type can't be encoded.
+	ErrInvalidType = errors.New("can't encode this type")
+	// ErrNoEncoderForType represents the error when an encoder couldn't be found for a type.
+	ErrNoEncoderForType = errors.New("no encoder for this type found")
+)
+
 // Encoder encodes the thumbnail to a specific format.
 type Encoder interface {
 	// Encode encodes the image to a format.
-	Encode(io.Writer, image.Image) error
+	Encode(io.Writer, interface{}) error
 	// Types returns the formats suffixes.
 	Types() []string
 	// MimeType returns the mimetype used by the encoder.
@@ -22,13 +38,17 @@ type Encoder interface {
 type PngEncoder struct{}
 
 // Encode encodes to png format
-func (e PngEncoder) Encode(w io.Writer, i image.Image) error {
-	return png.Encode(w, i)
+func (e PngEncoder) Encode(w io.Writer, img interface{}) error {
+	m, ok := img.(image.Image)
+	if !ok {
+		return ErrInvalidType
+	}
+	return png.Encode(w, m)
 }
 
 // Types returns the png suffix
 func (e PngEncoder) Types() []string {
-	return []string{"png"}
+	return []string{typePng}
 }
 
 // MimeType returns the mimetype for png files.
@@ -40,13 +60,17 @@ func (e PngEncoder) MimeType() string {
 type JpegEncoder struct{}
 
 // Encode encodes to jpg
-func (e JpegEncoder) Encode(w io.Writer, i image.Image) error {
-	return jpeg.Encode(w, i, nil)
+func (e JpegEncoder) Encode(w io.Writer, img interface{}) error {
+	m, ok := img.(image.Image)
+	if !ok {
+		return ErrInvalidType
+	}
+	return jpeg.Encode(w, m, nil)
 }
 
 // Types returns the jpg suffixes.
 func (e JpegEncoder) Types() []string {
-	return []string{"jpeg", "jpg"}
+	return []string{typeJpeg, typeJpg}
 }
 
 // MimeType returns the mimetype for jpg files.
@@ -54,15 +78,35 @@ func (e JpegEncoder) MimeType() string {
 	return "image/jpeg"
 }
 
+type GifEncoder struct{}
+
+func (e GifEncoder) Encode(w io.Writer, img interface{}) error {
+	g, ok := img.(*gif.GIF)
+	if !ok {
+		return ErrInvalidType
+	}
+	return gif.EncodeAll(w, g)
+}
+
+func (e GifEncoder) Types() []string {
+	return []string{typeGif}
+}
+
+func (e GifEncoder) MimeType() string {
+	return "image/gif"
+}
+
 // EncoderForType returns the encoder for a given file type
 // or nil if the type is not supported.
-func EncoderForType(fileType string) Encoder {
+func EncoderForType(fileType string) (Encoder, error) {
 	switch strings.ToLower(fileType) {
-	case "png":
-		return PngEncoder{}
-	case "jpg", "jpeg":
-		return JpegEncoder{}
+	case typePng:
+		return PngEncoder{}, nil
+	case typeJpg, typeJpeg:
+		return JpegEncoder{}, nil
+	case typeGif:
+		return GifEncoder{}, nil
 	default:
-		return nil
+		return nil, ErrNoEncoderForType
 	}
 }
diff --git a/thumbnails/pkg/thumbnail/generator.go b/thumbnails/pkg/thumbnail/generator.go
new file mode 100644
index 00000000000..6bea9b87a3b
--- /dev/null
+++ b/thumbnails/pkg/thumbnail/generator.go
@@ -0,0 +1,65 @@
+package thumbnail
+
+import (
+	"errors"
+	"image"
+	"image/draw"
+	"image/gif"
+	"strings"
+
+	"github.com/disintegration/imaging"
+)
+
+var (
+	// ErrInvalidType represents the error when a type can't be encoded.
+	ErrInvalidType2 = errors.New("can't encode this type")
+	// ErrNoGeneratorForType represents the error when no generator could be found for a type.
+	ErrNoGeneratorForType = errors.New("no generator for this type found")
+)
+
+type Generator interface {
+	GenerateThumbnail(image.Rectangle, interface{}) (interface{}, error)
+}
+
+type SimpleGenerator struct{}
+
+func (g SimpleGenerator) GenerateThumbnail(size image.Rectangle, img interface{}) (interface{}, error) {
+	m, ok := img.(image.Image)
+	if !ok {
+		return nil, ErrInvalidType2
+	}
+
+	return imaging.Thumbnail(m, size.Dx(), size.Dy(), imaging.Lanczos), nil
+}
+
+type GifGenerator struct{}
+
+func (g GifGenerator) GenerateThumbnail(size image.Rectangle, img interface{}) (interface{}, error) {
+	m, ok := img.(*gif.GIF)
+	if !ok {
+		return nil, ErrInvalidType2
+	}
+	var bounds image.Rectangle
+	for i := range m.Image {
+		img := imaging.Resize(m.Image[i], size.Dx(), size.Dy(), imaging.Lanczos)
+		bounds = image.Rect(0, 0, size.Dx(), size.Dy())
+		m.Image[i] = image.NewPaletted(bounds, m.Image[i].Palette)
+		draw.Draw(m.Image[i], bounds, img, image.Pt(0, 0), draw.Src)
+	}
+	m.Config.Height = bounds.Dy()
+	m.Config.Width = bounds.Dx()
+	return m, nil
+}
+
+// GeneratorForType returns the generator for a given file type
+// or nil if the type is not supported.
+func GeneratorForType(fileType string) (Generator, error) {
+	switch strings.ToLower(fileType) {
+	case typePng, typeJpg, typeJpeg:
+		return SimpleGenerator{}, nil
+	case typeGif:
+		return GifGenerator{}, nil
+	default:
+		return nil, ErrNoEncoderForType
+	}
+}
diff --git a/thumbnails/pkg/thumbnail/storage/filesystem.go b/thumbnails/pkg/thumbnail/storage/filesystem.go
index 604ca13c55e..980adedd32b 100644
--- a/thumbnails/pkg/thumbnail/storage/filesystem.go
+++ b/thumbnails/pkg/thumbnail/storage/filesystem.go
@@ -1,12 +1,14 @@
 package storage
 
 import (
-	"github.com/owncloud/ocis/ocis-pkg/log"
-	"github.com/owncloud/ocis/thumbnails/pkg/config"
-	"github.com/pkg/errors"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"strconv"
+
+	"github.com/owncloud/ocis/ocis-pkg/log"
+	"github.com/owncloud/ocis/thumbnails/pkg/config"
+	"github.com/pkg/errors"
 )
 
 const (
@@ -14,8 +16,8 @@ const (
 )
 
 // NewFileSystemStorage creates a new instance of FileSystem
-func NewFileSystemStorage(cfg config.FileSystemStorage, logger log.Logger) *FileSystem {
-	return &FileSystem{
+func NewFileSystemStorage(cfg config.FileSystemStorage, logger log.Logger) FileSystem {
+	return FileSystem{
 		root:   cfg.RootDirectory,
 		logger: logger,
 	}
@@ -27,21 +29,27 @@ type FileSystem struct {
 	logger log.Logger
 }
 
-// Get loads the image from the file system.
-func (s *FileSystem) Get(key string) ([]byte, bool) {
+func (s FileSystem) Stat(key string) bool {
+	img := filepath.Join(s.root, filesDir, key)
+	if _, err := os.Stat(img); err != nil {
+		return false
+	}
+	return true
+}
+
+func (s FileSystem) Get(key string) ([]byte, error) {
 	img := filepath.Join(s.root, filesDir, key)
 	content, err := os.ReadFile(img)
 	if err != nil {
-		if !os.IsNotExist(err) {
+		if !errors.Is(err, fs.ErrNotExist) {
 			s.logger.Debug().Str("err", err.Error()).Str("key", key).Msg("could not load thumbnail from store")
 		}
-		return nil, false
+		return nil, err
 	}
-	return content, true
+	return content, nil
 }
 
-// Set writes the image to the file system.
-func (s *FileSystem) Put(key string, img []byte) error {
+func (s FileSystem) Put(key string, img []byte) error {
 	imgPath := filepath.Join(s.root, filesDir, key)
 	dir := filepath.Dir(imgPath)
 	if err := os.MkdirAll(dir, 0700); err != nil {
@@ -71,7 +79,7 @@ func (s *FileSystem) Put(key string, img []byte) error {
 // e.g. 97/9f/4c8db98f7b82e768ef478d3c8612/500x300.png
 //
 // The key also represents the path to the thumbnail in the filesystem under the configured root directory.
-func (s *FileSystem) BuildKey(r Request) string {
+func (s FileSystem) BuildKey(r Request) string {
 	checksum := r.Checksum
 	filetype := r.Types[0]
 	filename := strconv.Itoa(r.Resolution.Dx()) + "x" + strconv.Itoa(r.Resolution.Dy()) + "." + filetype
diff --git a/thumbnails/pkg/thumbnail/storage/inmemory.go b/thumbnails/pkg/thumbnail/storage/inmemory.go
index 7ec358ca420..1c7befd888e 100644
--- a/thumbnails/pkg/thumbnail/storage/inmemory.go
+++ b/thumbnails/pkg/thumbnail/storage/inmemory.go
@@ -17,9 +17,14 @@ type InMemory struct {
 	store map[string][]byte
 }
 
+func (s InMemory) Stat(key string) bool {
+	_, exists := s.store[key]
+	return exists
+}
+
 // Get loads the thumbnail from memory.
-func (s InMemory) Get(key string) ([]byte, bool) {
-	return s.store[key], true
+func (s InMemory) Get(key string) ([]byte, error) {
+	return s.store[key], nil
 }
 
 // Set stores the thumbnail in memory.
diff --git a/thumbnails/pkg/thumbnail/storage/storage.go b/thumbnails/pkg/thumbnail/storage/storage.go
index bf7f3c54546..691fdca5af3 100644
--- a/thumbnails/pkg/thumbnail/storage/storage.go
+++ b/thumbnails/pkg/thumbnail/storage/storage.go
@@ -8,18 +8,19 @@ import (
 type Request struct {
 	// The checksum of the source file
 	// Will be used to determine if a thumbnail exists
-	Checksum   string
+	Checksum string
 	// Types provided by the encoder.
 	// Contains the mimetypes of the thumbnail.
 	// In case of jpg/jpeg it will contain both.
-	Types      []string
+	Types []string
 	// The resolution of the thumbnail
 	Resolution image.Rectangle
 }
 
 // Storage defines the interface for a thumbnail store.
 type Storage interface {
-	Get(string) ([]byte, bool)
+	Stat(string) bool
+	Get(string) ([]byte, error)
 	Put(string, []byte) error
 	BuildKey(Request) string
 }
diff --git a/thumbnails/pkg/thumbnail/thumbnail.go b/thumbnails/pkg/thumbnail/thumbnail.go
index 815c8928a33..b4f2f7b80c5 100644
--- a/thumbnails/pkg/thumbnail/thumbnail.go
+++ b/thumbnails/pkg/thumbnail/thumbnail.go
@@ -2,21 +2,22 @@ package thumbnail
 
 import (
 	"bytes"
-	"github.com/disintegration/imaging"
-	"github.com/owncloud/ocis/ocis-pkg/log"
-	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
 	"image"
+	"image/gif"
 	"mime"
-	"strings"
+
+	"github.com/owncloud/ocis/ocis-pkg/log"
+	"github.com/owncloud/ocis/thumbnails/pkg/thumbnail/storage"
 )
 
 var (
-	SupportedMimeTypes = [...]string{
-		"image/png",
-		"image/jpg",
-		"image/jpeg",
-		"image/gif",
-		"text/plain",
+	// SupportedMimeTypes contains a all mimetypes which are supported by the thumbnailer.
+	SupportedMimeTypes = map[string]struct{}{
+		"image/png":  {},
+		"image/jpg":  {},
+		"image/jpeg": {},
+		"image/gif":  {},
+		"text/plain": {},
 	}
 )
 
@@ -24,16 +25,20 @@ var (
 type Request struct {
 	Resolution image.Rectangle
 	Encoder    Encoder
+	Generator  Generator
 	Checksum   string
 }
 
 // Manager is responsible for generating thumbnails
 type Manager interface {
-	// Generate will return a thumbnail for a file
-	Generate(Request, image.Image) ([]byte, error)
-	// Get loads the thumbnail from the storage.
-	// It will return nil if no image is stored for the given context.
-	Get(Request) ([]byte, bool)
+	// Generate creates a thumbnail and stores it.
+	// The function returns a key with which the actual file can be retrieved.
+	Generate(Request, interface{}) (string, error)
+	// CheckThumbnail checks if a thumbnail with the requested attributes exists.
+	// The function will return a status if the file exists and the key to the file.
+	CheckThumbnail(Request) (string, bool)
+	// GetThumbnail will load the thumbnail from the storage and return its content.
+	GetThumbnail(key string) ([]byte, error)
 }
 
 // NewSimpleManager creates a new instance of SimpleManager
@@ -52,35 +57,40 @@ type SimpleManager struct {
 	resolutions Resolutions
 }
 
-// Generate creates a thumbnail and stores it.
-// The created thumbnail is also being returned.
-func (s SimpleManager) Generate(r Request, img image.Image) ([]byte, error) {
-	match := s.resolutions.ClosestMatch(r.Resolution, img.Bounds())
-	thumbnail := s.generate(match, img)
+func (s SimpleManager) Generate(r Request, img interface{}) (string, error) {
+	var match image.Rectangle
+	switch m := img.(type) {
+	case *gif.GIF:
+		match = s.resolutions.ClosestMatch(r.Resolution, m.Image[0].Bounds())
+	case image.Image:
+		match = s.resolutions.ClosestMatch(r.Resolution, m.Bounds())
+	}
 
-	dst := new(bytes.Buffer)
-	err := r.Encoder.Encode(dst, thumbnail)
+	thumbnail, err := r.Generator.GenerateThumbnail(match, img)
 	if err != nil {
-		return nil, err
+		return "", err
+	}
+
+	buf := new(bytes.Buffer)
+	if err := r.Encoder.Encode(buf, thumbnail); err != nil {
+		return "", err
 	}
 
 	k := s.storage.BuildKey(mapToStorageRequest(r))
-	err = s.storage.Put(k, dst.Bytes())
-	if err != nil {
-		s.logger.Warn().Err(err).Msg("could not store thumbnail")
+	if err := s.storage.Put(k, buf.Bytes()); err != nil {
+		s.logger.Error().Err(err).Msg("could not store thumbnail")
+		return "", err
 	}
-	return dst.Bytes(), nil
+	return k, nil
 }
 
-// Get tries to get the stored thumbnail and return it.
-// If there is no cached thumbnail it will return nil
-func (s SimpleManager) Get(r Request) ([]byte, bool) {
+func (s SimpleManager) CheckThumbnail(r Request) (string, bool) {
 	k := s.storage.BuildKey(mapToStorageRequest(r))
-	return s.storage.Get(k)
+	return k, s.storage.Stat(k)
 }
 
-func (s SimpleManager) generate(r image.Rectangle, img image.Image) image.Image {
-	return imaging.Thumbnail(img, r.Dx(), r.Dy(), imaging.Lanczos)
+func (s SimpleManager) GetThumbnail(key string) ([]byte, error) {
+	return s.storage.Get(key)
 }
 
 func mapToStorageRequest(r Request) storage.Request {
@@ -96,10 +106,6 @@ func IsMimeTypeSupported(m string) bool {
 	if err != nil {
 		return false
 	}
-	for _, mt := range SupportedMimeTypes {
-		if strings.EqualFold(mt, mimeType) {
-			return true
-		}
-	}
-	return false
+	_, supported := SupportedMimeTypes[mimeType]
+	return supported
 }
diff --git a/webdav/pkg/service/v0/service.go b/webdav/pkg/service/v0/service.go
index 40eefeff01d..c98a3b3afd0 100644
--- a/webdav/pkg/service/v0/service.go
+++ b/webdav/pkg/service/v0/service.go
@@ -2,6 +2,7 @@ package svc
 
 import (
 	"encoding/xml"
+	"io"
 	"net/http"
 	"path/filepath"
 	"strings"
@@ -128,12 +129,7 @@ func (g Webdav) SpacesThumbnail(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if len(rsp.Thumbnail) == 0 {
-		renderError(w, r, errNotFound(""))
-		return
-	}
-
-	g.mustRender(w, r, newThumbnailResponse(rsp))
+	g.sendThumbnailResponse(rsp, w, r)
 }
 
 // Thumbnail implements the Service interface.
@@ -186,12 +182,7 @@ func (g Webdav) Thumbnail(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if len(rsp.Thumbnail) == 0 {
-		renderError(w, r, errNotFound(""))
-		return
-	}
-
-	g.mustRender(w, r, newThumbnailResponse(rsp))
+	g.sendThumbnailResponse(rsp, w, r)
 }
 
 func (g Webdav) PublicThumbnail(w http.ResponseWriter, r *http.Request) {
@@ -231,12 +222,7 @@ func (g Webdav) PublicThumbnail(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if len(rsp.Thumbnail) == 0 {
-		renderError(w, r, errNotFound(""))
-		return
-	}
-
-	g.mustRender(w, r, newThumbnailResponse(rsp))
+	g.sendThumbnailResponse(rsp, w, r)
 }
 
 func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) {
@@ -247,7 +233,7 @@ func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	rsp, err := g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
+	_, err = g.thumbnailsClient.GetThumbnail(r.Context(), &thumbnailssvc.GetThumbnailRequest{
 		Filepath:      strings.TrimLeft(tr.Filepath, "/"),
 		ThumbnailType: extensionToThumbnailType(strings.TrimLeft(tr.Extension, ".")),
 		Width:         tr.Width,
@@ -276,26 +262,56 @@ func (g Webdav) PublicThumbnailHead(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if len(rsp.Thumbnail) == 0 {
-		renderError(w, r, errNotFound(""))
+	w.WriteHeader(http.StatusOK)
+}
+
+func (g Webdav) sendThumbnailResponse(rsp *thumbnailssvc.GetThumbnailResponse, w http.ResponseWriter, r *http.Request) {
+	client := &http.Client{
+		// Timeout: time.Second * 5,
+	}
+
+	dlReq, err := http.NewRequest(http.MethodGet, rsp.DataEndpoint, http.NoBody)
+	if err != nil {
+		renderError(w, r, errInternalError(err.Error()))
+		g.log.Error().Err(err).Msg("could not download thumbnail")
+		return
+	}
+	dlReq.Header.Set("Transfer-Token", rsp.TransferToken)
+
+	dlRsp, err := client.Do(dlReq)
+	if err != nil {
+		renderError(w, r, errInternalError(err.Error()))
+		g.log.Error().Err(err).Msg("could not download thumbnail")
+		return
+	}
+	defer dlRsp.Body.Close()
+
+	if dlRsp.StatusCode != http.StatusOK {
+		g.log.Error().
+			Str("transfer_token", rsp.TransferToken).
+			Str("data_endpoint", rsp.DataEndpoint).
+			Str("response_status", dlRsp.Status).
+			Msg("could not download thumbnail")
+		renderError(w, r, errInternalError("could not download thumbnail"))
 		return
 	}
 
 	w.WriteHeader(http.StatusOK)
+	w.Header().Set("Content-Type", rsp.Mimetype)
+	_, err = io.Copy(w, dlRsp.Body)
+	if err != nil {
+		g.log.Error().Err(err).Msg("failed to write thumbnail to response writer")
+	}
 }
 
-func extensionToThumbnailType(ext string) thumbnailssvc.GetThumbnailRequest_ThumbnailType {
+func extensionToThumbnailType(ext string) thumbnailsmsg.ThumbnailType {
 	switch strings.ToUpper(ext) {
-	case "GIF", "PNG":
-		return thumbnailssvc.GetThumbnailRequest_PNG
+	case "GIF":
+		return thumbnailsmsg.ThumbnailType_GIF
+	case "PNG":
+		return thumbnailsmsg.ThumbnailType_PNG
 	default:
-		return thumbnailssvc.GetThumbnailRequest_JPG
-	}
-}
-
-func (g Webdav) mustRender(w http.ResponseWriter, r *http.Request, renderer render.Renderer) {
-	if err := render.Render(w, r, renderer); err != nil {
-		g.log.Err(err).Msg("failed to write response")
+		return thumbnailsmsg.ThumbnailType_JPG
 	}
 }
 
@@ -335,25 +351,6 @@ func errNotFound(msg string) *errResponse {
 	return newErrResponse(http.StatusNotFound, msg)
 }
 
-type thumbnailResponse struct {
-	contentType string
-	thumbnail   []byte
-}
-
-func (t *thumbnailResponse) Render(w http.ResponseWriter, _ *http.Request) error {
-	w.WriteHeader(http.StatusOK)
-	w.Header().Set("Content-Type", t.contentType)
-	_, err := w.Write(t.thumbnail)
-	return err
-}
-
-func newThumbnailResponse(rsp *thumbnailssvc.GetThumbnailResponse) *thumbnailResponse {
-	return &thumbnailResponse{
-		contentType: rsp.Mimetype,
-		thumbnail:   rsp.Thumbnail,
-	}
-}
-
 func renderError(w http.ResponseWriter, r *http.Request, err *errResponse) {
 	render.Status(r, err.HTTPStatusCode)
 	render.XML(w, r, err)


Test patch:
diff --git a/tests/acceptance/features/apiSpaces/changeSpaces.feature b/tests/acceptance/features/apiSpaces/changeSpaces.feature
index b132f0e14f6..593291de4ae 100644
--- a/tests/acceptance/features/apiSpaces/changeSpaces.feature
+++ b/tests/acceptance/features/apiSpaces/changeSpaces.feature
@@ -33,3 +33,44 @@ Feature: Change data of space
       | key              | value         |
       | name             | Project Earth |
       | quota@@@total    | 100           |
+
+
+  Scenario: An user set readme file as description of the space via the Graph API
+    Given user "Alice" has created a space "add special section" with the default quota using the GraphApi
+    And user "Alice" has created a folder ".space" in space "add special section"
+    And user "Alice" has uploaded a file inside space "add special section" with content "space description" to ".space/readme.md"
+    When user "Alice" sets the file ".space/readme.md" as a description in a special section of the "add special section" space
+    Then the HTTP status code should be "200"
+    When user "Alice" lists all available spaces via the GraphApi
+    Then the json responded should contain a space "add special section" owned by "Alice" with description file ".space/readme.md" with these key and value pairs:
+      | key                                | value               |
+      | name                               | add special section |
+      | special@@@0@@@size                 | 17                  |
+      | special@@@0@@@name                 | readme.md           |
+      | special@@@0@@@specialFolder@@@name | readme              |
+      | special@@@0@@@file@@@mimeType       | text/markdown       |
+      | special@@@0@@@id                   | %file_id%            |
+      | special@@@0@@@eTag                 | %eTag%              |
+
+
+  Scenario Outline: An user set image file as space image of the space via the Graph API
+    Given user "Alice" has created a space "add special section" with the default quota using the GraphApi
+    And user "Alice" has created a folder ".space" in space "add special section"
+    And user "Alice" has uploaded a file inside space "add special section" with content "" to "<fileName>"
+    When user "Alice" sets the file "<fileName>" as a space image in a special section of the "add special section" space
+    Then the HTTP status code should be "200"
+    When user "Alice" lists all available spaces via the GraphApi
+    Then the json responded should contain a space "add special section" owned by "Alice" with description file "<fileName>" with these key and value pairs:
+      | key                                | value               |
+      | name                               | add special section |
+      | special@@@0@@@size                 | 0                   |
+      | special@@@0@@@name                 | <nameInResponse>    |
+      | special@@@0@@@specialFolder@@@name | image               |
+      | special@@@0@@@file@@@mimeType       | <mimeType>          |
+      | special@@@0@@@id                   | %file_id%            |
+      | special@@@0@@@eTag                 | %eTag%              |
+    Examples:
+      | fileName                | nameInResponse  | mimeType   |
+      | .space/spaceImage.jpeg | spaceImage.jpeg | image/jpeg |
+      | .space/spaceImage.png  | spaceImage.png  | image/png  |
+      | .space/spaceImage.gif  | spaceImage.gif  | image/gif  |
diff --git a/tests/acceptance/features/bootstrap/SpacesContext.php b/tests/acceptance/features/bootstrap/SpacesContext.php
index ca88e18d3cf..da3b7370217 100644
--- a/tests/acceptance/features/bootstrap/SpacesContext.php
+++ b/tests/acceptance/features/bootstrap/SpacesContext.php
@@ -205,6 +205,65 @@ class SpacesContext implements Context {
 		return $spaces[$spaceName];
 	}
 
+	/**
+	 * The method finds file by fileName and spaceName and returns data of file wich contains in responseHeader
+	 * fileName contains the path, if the file is in the folder
+	 *
+	 * @param string $user
+	 * @param string $spaceName
+	 * @param string $fileName
+	 *
+	 * @return array
+	 */
+	public function getFileData(string $user, string $spaceName, string $fileName): array {
+		$space = $this->getSpaceByName($user, $spaceName);
+		$fullUrl = $this->baseUrl . "/remote.php/dav/spaces/" . $space["id"] . "/" . $fileName;
+		
+		$this->featureContext->setResponse(
+			HttpRequestHelper::get(
+				$fullUrl,
+				"",
+				$user,
+				$this->featureContext->getPasswordForUser($user),
+				[],
+				"{}"
+			)
+		);
+		$this->featureContext->theHTTPStatusCodeShouldBe(
+			200,
+			"file $fileName not found"
+		);
+		return $this->featureContext->getResponse()->getHeaders();
+	}
+
+	/**
+	 * The method returns fileId
+	 *
+	 * @param string $user
+	 * @param string $spaceName
+	 * @param string $fileName
+	 *
+	 * @return string
+	 */
+	public function getFileId(string $user, string $spaceName, string $fileName): string {
+		$fileData = $this->getFileData($user, $spaceName, $fileName);
+		return $fileData["Oc-Fileid"][0];
+	}
+
+	/**
+	 * The method returns eTag
+	 *
+	 * @param string $user
+	 * @param string $spaceName
+	 * @param string $fileName
+	 *
+	 * @return string
+	 */
+	public function getETag(string $user, string $spaceName, string $fileName): string {
+		$fileData = $this->getFileData($user, $spaceName, $fileName);
+		return $fileData["Etag"][0];
+	}
+
 	/**
 	 * The method returns userId
 	 *
@@ -213,7 +272,6 @@ class SpacesContext implements Context {
 	 * @return string
 	 */
 	public function getUserIdByUserName(string $userName): string {
-
 		$fullUrl = $this->baseUrl . "/api/v0/accounts/accounts-list";
 		$this->featureContext->setResponse(
 			HttpRequestHelper::post(
@@ -430,7 +488,7 @@ class SpacesContext implements Context {
 			$this->listMySpacesRequest(
 				$user,
 				$this->featureContext->getPasswordForUser($user),
-				"?". $query
+				"?" . $query
 			)
 		);
 	}
@@ -755,7 +813,7 @@ class SpacesContext implements Context {
 	}
 
 	/**
-	 * @Then /^the json responded should contain a space "([^"]*)" (?:|(?:owned by|granted to) "([^"]*)" )with these key and value pairs:$/
+	 * @Then /^the json responded should contain a space "([^"]*)" (?:|(?:owned by|granted to) "([^"]*)" )(?:|(?:with description file|with space image) "([^"]*)" )with these key and value pairs:$/
 	 *
 	 * @param string $spaceName
 	 * @param string $userName
@@ -767,6 +825,7 @@ class SpacesContext implements Context {
 	public function jsonRespondedShouldContain(
 		string $spaceName,
 		string $userName = '',
+		string $fileName = '',
 		TableNode $table
 	): void {
 		$this->featureContext->verifyTableNodeColumns($table, ['key', 'value']);
@@ -791,6 +850,18 @@ class SpacesContext implements Context {
 							[$this, "getUserIdByUserName"],
 						"parameter" => [$userName]
 					],
+					[
+						"code" => "%file_id%",
+						"function" =>
+							[$this, "getFileId"],
+						"parameter" => [$userName, $spaceName, $fileName]
+					],
+					[
+						"code" => "%eTag%",
+						"function" =>
+							[$this, "getETag"],
+						"parameter" => [$userName, $spaceName, $fileName]
+					],
 				]
 			);
 			$segments = explode("@@@", $row["key"]);
@@ -830,10 +901,11 @@ class SpacesContext implements Context {
 		
 		$userRole = "";
 		foreach ($permissions as $permission) {
-			foreach ($permission["grantedTo"] as $grantedTo)
-			if ($grantedTo["user"]["id"] === $userId) {
-				$userRole = $permission["roles"][0];
-			} 
+			foreach ($permission["grantedTo"] as $grantedTo) {
+				if ($grantedTo["user"]["id"] === $userId) {
+					$userRole = $permission["roles"][0];
+				}
+			}
 		}
 		Assert::assertEquals($userRole, $role, "the user $userName with the role $role could not be found");
 	}
@@ -875,18 +947,18 @@ class SpacesContext implements Context {
 			)
 		);
 		$matches = [];
-		foreach($spaces["value"] as $space) {
-			if($onlyOrNot === "not") {
+		foreach ($spaces["value"] as $space) {
+			if ($onlyOrNot === "not") {
 				Assert::assertNotEquals($space["driveType"], $type);
 			}
-			if($onlyOrNot === "only") {
+			if ($onlyOrNot === "only") {
 				Assert::assertEquals($space["driveType"], $type);
 			}
-			if($onlyOrNot === "" && $space["driveType"] === $type) {
+			if ($onlyOrNot === "" && $space["driveType"] === $type) {
 				$matches[] = $space;
 			}
 		}
-		if($onlyOrNot === "") {
+		if ($onlyOrNot === "") {
 			Assert::assertNotEmpty($matches);
 		}
 	}
@@ -1031,7 +1103,7 @@ class SpacesContext implements Context {
 	): void {
 		$exploded = explode('/', $folder);
 		$path = '';
-		for ($i = 0; $i < count($exploded); $i++) {
+		for ($i = 0; $i < \count($exploded); $i++) {
 			$path = $path . $exploded[$i] . '/';
 			$this->theUserCreatesAFolderToAnotherOwnerSpaceUsingTheGraphApi($user, $path, $spaceName);
 		};
@@ -1225,36 +1297,36 @@ class SpacesContext implements Context {
 	}
 
 	/**
-    	 * @When /^user "([^"]*)" changes the description of the "([^"]*)" space to "([^"]*)"$/
-    	 *
-    	 * @param string $user
-    	 * @param string $spaceName
-    	 * @param string $newName
-    	 *
-    	 * @return void
-    	 * @throws GuzzleException
-    	 * @throws Exception
-    	 */
-    	public function updateSpaceDescription(
-    		string $user,
-    		string $spaceName,
-    		string $newDescription
-    	): void {
-    		$space = $this->getSpaceByName($user, $spaceName);
-    		$spaceId = $space["id"];
-
-    		$bodyData = ["description" => $newDescription];
-    		$body = json_encode($bodyData, JSON_THROW_ON_ERROR);
-
-    		$this->featureContext->setResponse(
-    			$this->sendUpdateSpaceRequest(
-    				$user,
-    				$this->featureContext->getPasswordForUser($user),
-    				$body,
-    				$spaceId
-    			)
-    		);
-    	}
+	 * @When /^user "([^"]*)" changes the description of the "([^"]*)" space to "([^"]*)"$/
+	 *
+	 * @param string $user
+	 * @param string $spaceName
+	 * @param string $newName
+	 *
+	 * @return void
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function updateSpaceDescription(
+		string $user,
+		string $spaceName,
+		string $newDescription
+	): void {
+		$space = $this->getSpaceByName($user, $spaceName);
+		$spaceId = $space["id"];
+
+		$bodyData = ["description" => $newDescription];
+		$body = json_encode($bodyData, JSON_THROW_ON_ERROR);
+
+		$this->featureContext->setResponse(
+			$this->sendUpdateSpaceRequest(
+				$user,
+				$this->featureContext->getPasswordForUser($user),
+				$body,
+				$spaceId
+			)
+		);
+	}
 
 	/**
 	 * @When /^user "([^"]*)" changes the quota of the "([^"]*)" space to "([^"]*)"$/
@@ -1288,6 +1360,47 @@ class SpacesContext implements Context {
 		);
 	}
 
+	/**
+	 * @When /^user "([^"]*)" sets the file "([^"]*)" as a (description|space image)\s? in a special section of the "([^"]*)" space$/
+	 *
+	 * @param string $user
+	 * @param string $file
+	 * @param string $type
+	 * @param string $spaceName
+	 *
+	 * @return void
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function updateSpaceSpecialSection(
+		string $user,
+		string $file,
+		string $type,
+		string $spaceName
+	): void {
+		$space = $this->getSpaceByName($user, $spaceName);
+		$spaceId = $space["id"];
+		$fileId = $this->getFileId($user, $spaceName, $file);
+
+		if ($type === "description") {
+			$type = "readme";
+		} else {
+			$type = "image";
+		}
+
+		$bodyData = ["special" => [["specialFolder" => ["name" => "$type"], "id" => "$fileId"]]];
+		$body = json_encode($bodyData, JSON_THROW_ON_ERROR);
+
+		$this->featureContext->setResponse(
+			$this->sendUpdateSpaceRequest(
+				$user,
+				$this->featureContext->getPasswordForUser($user),
+				$body,
+				$spaceId
+			)
+		);
+	}
+
 	/**
 	 * Send Graph Update Space Request
 	 *
@@ -1347,6 +1460,36 @@ class SpacesContext implements Context {
 		);
 	}
 
+	/**
+	 * @When /^user "([^"]*)" has created a space "([^"]*)" with the default quota using the GraphApi$/
+	 *
+	 * @param string $user
+	 * @param string $spaceName
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws Exception
+	 */
+	public function theUserHasCreatedASpaceByDefaultUsingTheGraphApi(
+		string $user,
+		string $spaceName
+	): void {
+		$space = ["Name" => $spaceName];
+		$body = json_encode($space, JSON_THROW_ON_ERROR);
+		$this->featureContext->setResponse(
+			$this->sendCreateSpaceRequest(
+				$user,
+				$this->featureContext->getPasswordForUser($user),
+				$body
+			)
+		);
+		$this->featureContext->theHTTPStatusCodeShouldBe(
+			201,
+			"Expected response status code should be 201 (Created)"
+		);
+	}
+
 	/**
 	 * @Given /^user "([^"]*)" has uploaded a file inside space "([^"]*)" with content "([^"]*)" to "([^"]*)"$/
 	 *
@@ -1443,10 +1586,10 @@ class SpacesContext implements Context {
 
 		$expectedHTTPStatus = "200";
 		$this->featureContext->theHTTPStatusCodeShouldBe(
-            $expectedHTTPStatus,
+			$expectedHTTPStatus,
 			"Expected response status code should be $expectedHTTPStatus"
 		);
-        $expectedOCSStatus = "200";
+		$expectedOCSStatus = "200";
 		$this->ocsContext->theOCSStatusCodeShouldBe($expectedOCSStatus, "Expected OCS response status code $expectedOCSStatus");
 	}
 
@@ -1546,7 +1689,7 @@ class SpacesContext implements Context {
 		$this->sendDisableSpaceRequest($user, $spaceName);
 		$expectedHTTPStatus = "204";
 		$this->featureContext->theHTTPStatusCodeShouldBe(
-            $expectedHTTPStatus,
+			$expectedHTTPStatus,
 			"Expected response status code should be $expectedHTTPStatus"
 		);
 	}
@@ -1647,7 +1790,7 @@ class SpacesContext implements Context {
 		$this->sendRestoreSpaceRequest($user, $spaceName);
 		$expectedHTTPStatus = "200";
 		$this->featureContext->theHTTPStatusCodeShouldBe(
-            $expectedHTTPStatus,
+			$expectedHTTPStatus,
 			"Expected response status code should be $expectedHTTPStatus"
 		);
 	}
diff --git a/thumbnails/pkg/thumbnail/encoding_test.go b/thumbnails/pkg/thumbnail/encoding_test.go
index 1dfbef60aa6..6dcce72a621 100644
--- a/thumbnails/pkg/thumbnail/encoding_test.go
+++ b/thumbnails/pkg/thumbnail/encoding_test.go
@@ -14,7 +14,7 @@ func TestEncoderForType(t *testing.T) {
 	}
 
 	for k, v := range table {
-		e := EncoderForType(k)
+		e, _ := EncoderForType(k)
 		if e != v {
 			t.Fail()
 		}
diff --git a/thumbnails/pkg/thumbnail/thumbnail_test.go b/thumbnails/pkg/thumbnail/thumbnail_test.go
index 94bf0a27733..76be41a6688 100644
--- a/thumbnails/pkg/thumbnail/thumbnail_test.go
+++ b/thumbnails/pkg/thumbnail/thumbnail_test.go
@@ -33,14 +33,14 @@ func BenchmarkGet(b *testing.B) {
 	res, _ := ParseResolution("32x32")
 	req := Request{
 		Resolution: res,
-		Checksum:       "1872ade88f3013edeb33decd74a4f947",
+		Checksum:   "1872ade88f3013edeb33decd74a4f947",
 	}
 	cwd, _ := os.Getwd()
 	p := filepath.Join(cwd, "../../testdata/oc.png")
 	f, _ := os.Open(p)
 	defer f.Close()
 	img, ext, _ := image.Decode(f)
-	req.Encoder = EncoderForType(ext)
+	req.Encoder, _ = EncoderForType(ext)
 	for i := 0; i < b.N; i++ {
 		_, _ = sut.Generate(req, img)
 	}