Repository: owncloud/ocis
Pull request #3272 (2022-03-04 18:18:06)

Change thumbnails API and implement gif thumbnails
By popular demand, I have implement animated thumbnails for gifs.
~~This PR is not ready yet. I want to refactor the thumbnail code a little bit more and also the download of the thumbnails from the thumbnails service shouldn't be done via grpc. I have run into the problem that the gif thumbnails were bigger than the allowed grpc message size. So to prevent this I would implement a similar solution like the downloads in reva were there is a http endpoint for the actual download.~~

API changes are implemented.

Here is a preview of the animated thumbnails:

https://user-images.githubusercontent.com/5579653/156819409-a87c2eba-aa67-4269-b29e-9c3095acc18f.mp4

In addition the thumbnails API was changed a bit because GRPC only allows certain message sizes and while testing some gif thumbnails I already ran into the maximum. These changes allow bigger thumbnails. (file size not resolution, but it most often correlates so...)





Patch:
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/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/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)
 	}