Skip to Content
Technical Articles
Author's profile photo Mio Yasutake

My journey towards using UI5 UploadSet with CAP backend

Introduction

CAP is capable of storing media data. But does it work with UI5 upload controls such as UploadSet(*)?
The answer to above question is yes. However, UploadSet doesn’t work out-of-the-box with CAP so you need to write some code to make it adapt to CAP.

The following are the challenges when you try to integrate UploadSet with CAP.

  1. CAP expects to receive media data with PUT request, while UploadSet sends media data by POST request by default.
  2. In order to open a file (picture) in UploadSet, it has to be fetched in blob, which doesn’t happen naturally with UploadSet.

The good news is, you don’t have to develop a custom control to upload / download files to / from CAP.
UploadSet has several “hooks” for your own logic and with the help of those hooks, you can integrate UploadSet with CAP.

In this blog I’m going to develop a CAP service (Odata v4) and a simple UI5 app with UpaloadSet.
The code is available at the following GitHub repository.
https://github.com/miyasuta/cap-media

* UploadSet is a successor of UploadCollection, as UploadCollection was deprecated as of UI5 version 1.88.

 

Development

  1. CAP project
  2. UI5 app
    1. Uploading files
    2. Downloading files
    3. Set meaningful names to downloaded files (optional)
    4. Show file icons (optional)

* The optional steps are not directly related to UploadSet + CAP integration, but rather to improve the appearance of the app.

 

1. CAP project

1.1. db/data-model.cds

Important annotations are below.

@Core.MediaType: Indicates that the element contains media data. The value of this annotation is either a string with the contained MIME type or is a path to the element that contains the MIME type.
@Core.IsMediaType: Indicates that the element contains a MIME type

namespace miyasuta.media;

using {
    cuid,
    managed
} from '@sap/cds/common';

entity Files: cuid, managed{
    @Core.MediaType: mediaType
    content: LargeBinary;
    @Core.IsMediaType: true
    mediaType: String;
    fileName: String;
    size: Integer;
    url: String;
}

 

1.2. srv/media-service.cds

There’s nothing special here. Just exposing Files entity to the service.

using { miyasuta.media as db } from '../db/data-model';

service Attachments {
    entity Files as projection on db.Files
}

 

1.3. srv/media-service.js

I’ve implemented create handler for filling download URL (just for convenience of the UI).

module.exports = async function () {
    this.before('CREATE', 'Files', req => {
        console.log('Create called')
        console.log(JSON.stringify(req.data))
        req.data.url = `/attachments/Files(${req.data.ID})/content`
    })
}

 

2. UI5 app

2.1. Uploading files

First, let’s focus on how to upload files.

manifest.json

The CAP OData service is added as datasource with path “/attachments”.

{
    "_version": "1.32.0",
    "sap.app": {
        ...
        "dataSources": {
            "mainService": {
                "uri": "/attachments/",
                "type": "OData",
                "settings": {
                    "odataVersion": "4.0",
                    "localUri": "localService/metadata.xml"
                }
            }
        },
    },
    ...
    "sap.ui5": {
       ...
        "models": {
            "i18n": {
                "type": "sap.ui.model.resource.ResourceModel",
                "settings": {
                    "bundleName": "miyasuta.attachments.i18n.i18n"
                }
            },
            "": {
                "type": "sap.ui.model.odata.v4.ODataModel",
                "settings": {
                  "synchronizationMode": "None",
                  "operationMode": "Server",
                  "autoExpandSelect": true,
                  "earlyRequests": true,
                  "groupProperties": {
                    "default": {
                      "submit": "Auto"
                    }
                  }
                },
                "dataSource": "mainService"
              }
        },

 

View

Let’s first examine UploadSet’s default upload behavior. To do that, I’ve created the following view, without event handlers for UploadSet.

<mvc:View
	controllerName="miyasuta.attachments.controller.App"
	xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true"
	xmlns="sap.m"
	xmlns:upload="sap.m.upload"
>
	<App id="app">
		<pages>
			<Page
				id="page"
				title="{i18n>title}"
			>
				<upload:UploadSet
					id="uploadSet"
					instantUpload="true"
					uploadEnabled="true"
					uploadUrl="/attachments/Files"				
					items="{
								path: '/Files',
								parameters: {
									$orderby: 'createdAt desc'
								},
								templateShareable: false}"
				>
					<upload:toolbar>
					</upload:toolbar>
					<upload:items>
						<upload:UploadSetItem
							fileName="{fileName}"
							mediaType="{mediaType}"
							url="{url}"
							enabledEdit="false"
							visibleEdit="false"
							openPressed="onOpenPressed"
						>
							<upload:attributes>
								<ObjectAttribute
									title="Uploaded By"
									text="{createdBy}"
									active="false"
								/>
								<ObjectAttribute
									title="Uploaded on"
									text="{createdAt}"
									active="false"
								/>
								<ObjectAttribute
									title="File Size"
									text="{size}"
									active="false"
								/>
							</upload:attributes>
						</upload:UploadSetItem>
					</upload:items>
				</upload:UploadSet>
			</Page>			
		</pages>
	</App>
</mvc:View>

 

The app looks like this.

 

when you upload a file, you’ll see an error below in the backend console. Here you find that data has been sent by POST request.

[cds] - POST /attachments/Files
[cds] - DeserializationError: No payload deserializer available for resource kind 'ENTITY' and mime type 'image/png'

 

To fix this, make the following changes to the UploadSet properties.

  • Set instantupload to “false” to prevent the default upload behavior
  • Remove uploadUrl, because we need to set this dynamically after receiving the entity’s key.
  • Add event handler for afterItemAdded event. We’ll be uploading a file here.
				<upload:UploadSet
					id="uploadSet"
					instantUpload="false"
					uploadEnabled="true"
					afterItemAdded="onAfterItemAdded"
					uploadCompleted="onUploadCompleted"					
					items="{
								path: '/Files',
								parameters: {
									$orderby: 'createdAt desc'
								},
								templateShareable: false}"
				>

 

Controller

The following is the initial state of the controller.
In the method onAfterItemAdded, we first create a new entity of File (method: _createEntity).
After receiving the entity’s key, we then construct an URL and upload a file by PUT request (method: _uploadContent).

sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/m/MessageToast"
],
	function (
		Controller,
		MessageToast
	) {
		"use strict";

		return Controller.extend("miyasuta.attachments.controller.App", {
			onInit: function () {
				
			},

			onAfterItemAdded: function (oEvent) {
				var item = oEvent.getParameter("item")
				this._createEntity(item)
				.then((id) => {
					this._uploadContent(item, id);
				})
				.catch((err) => {
					console.log(err);
				})
			},

			onUploadCompleted: function (oEvent) {
				var oUploadSet = this.byId("uploadSet");
				oUploadSet.removeAllIncompleteItems();
				oUploadSet.getBinding("items").refresh();
			},

			onOpenPressed: function (oEvent) {	
				// to be implemented			
			},

			_createEntity: function (item) {
					var data = {
						mediaType: item.getMediaType(),
						fileName: item.getFileName(),
						size: item.getFileObject().size
					};
	
					var settings = {
						url: "/attachments/Files",
						method: "POST",
						headers: {
							"Content-type": "application/json"
						},
						data: JSON.stringify(data)
					}
	
				return new Promise((resolve, reject) => {
					$.ajax(settings)
						.done((results, textStatus, request) => {
							resolve(results.ID);
						})
						.fail((err) => {
							reject(err);
						})
				})				
			},

			_uploadContent: function (item, id) {
				var url = `/attachments/Files(${id})/content`
				item.setUploadUrl(url);	
				var oUploadSet = this.byId("uploadSet");
				oUploadSet.setHttpRequestMethod("PUT")
				oUploadSet.uploadItem(item);
			}			
		});
	});

 

Now, if you upload an image file, you’ll see it successfully uploaded.

 

The screenshot below is the result of GET request from Postman. So far, so good!

URL: http://localhost:4004/attachments/Files(<uuid>)/content

 

But what if you click the link on the item?

 

A new browser tab opens with black screen with a small white square in the middle. This is NOT what I’ve uploaded (or, it is supposed to look)!

 

2.2. Downloading files

To fix above issue, we’ll implement onOpenPressed method and overwrite the default behavior. To open a picture properly, we need to specify response type as blob (see method: _download ).

			onOpenPressed: function (oEvent) {
				oEvent.preventDefault();
				var item = oEvent.getSource();
				this._download(item)
					.then((blob) => {
						var url = window.URL.createObjectURL(blob);
						//open in the browser
						window.open(url);					
					})
					.catch((err)=> {
						console.log(err);
					});					
			},


			_download: function (item) {
				var settings = {
					url: item.getUrl(),
					method: "GET",
					xhrFields:{
						responseType: "blob"
					}
				}	

				return new Promise((resolve, reject) => {
					$.ajax(settings)
					.done((result, textStatus, request) => {
						resolve(result);
					})
					.fail((err) => {
						reject(err);
					})
				});						
			},

 

As a result, the image is shown in the browser correctly.

 

2.3. Set meaningful names to downloaded files (optional)

While images, pdf files and text files are opened in a new browser tab, other types of files such as Word or Excel are downloaded to PC (I haven’t tested all file types).
Downloaded files get random guids as file name. Can we make the file names more meaningful ones?

 

To achieve this, I’ve changed the onOpenPressed method to download files, instead of using window.open() method. The following is the revised code.

			onOpenPressed: function (oEvent) {
				oEvent.preventDefault();
				var item = oEvent.getSource();
				this._fileName = item.getFileName();
				this._download(item)
					.then((blob) => {
						var url = window.URL.createObjectURL(blob);
						// //open in the browser
						// window.open(url);

						//download
						var link = document.createElement('a');
						link.href = url;
						link.setAttribute('download', this._fileName);
						document.body.appendChild(link);
						link.click();
						document.body.removeChild(link);						
					})
					.catch((err)=> {
						console.log(err);
					});					
			},

 

As a result, downloaded files get their original names.

 

2.4. Show file icons (optional)

You might remember, that when you used UploadCollection, you would see file icons to the left of file name as shown below.

 

Although UploadSet seems to reserve space for icons, icons are not displayed.
I found a similar case here, which hasn’t been resolved yet.

https://answers.sap.com/questions/13293377/uploadset-not-showing-icons.html

My workaround is to use thumbnailUrl property with formatter. In the formatter, I’ve set an icon URL according to mimeType. But I feel icons should be shown without writhing such code.
If someone knows a better way, please let me know in the comment section below.

						<upload:UploadSetItem
							fileName="{fileName}"
							mediaType="{mediaType}"
							url="{url}"
							thumbnailUrl="{
								path: 'mediaType',
								formatter: '.formatThumbnailUrl'
							}"							
							...
						>

This is formatter code.

			formatThumbnailUrl: function (mediaType) {
				var iconUrl;
				switch (mediaType) {
					case "image/png":
						iconUrl = "sap-icon://card";
						break;
					case "text/plain":
						iconUrl = "sap-icon://document-text";
						break;
					case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
						iconUrl = "sap-icon://excel-attachment";
						break;
					case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
						iconUrl = "sap-icon://doc-attachment";
						break;
					case "application/pdf":
						iconUrl = "sap-icon://pdf-attachment";
						break;
					default:
						iconUrl = "sap-icon://attachment";
				}
				return iconUrl;
			}

 

Finally, icons are displayed.

 

Conclusion

  • You can use UploadSet to upload / download files to / from CAP OData service.
  • To make UploadSet work with CAP, you need to:
    • send contents by PUT request, instead of POST request
    • downloaded contents in blob format

References

Assigned Tags

      6 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Sarath Raj
      Sarath Raj

      Excellent Blog!!

       

      Where are the files getting stored, is it using BTP object store.

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Sarath Raj,

      Thanks for your comment.

      The files are stored in a HANA Cloud Table. I'm not using Object Store.

       

       

      Author's profile photo Jhodel Cailan
      Jhodel Cailan

      Fantastic blog post Mio Yasutake !!

      I was in fact looking for a sample of UploadSet with CAP backend because UploadCollection is already deprecated, and your blog post really has done that.

      Just a minor note, that the current sample from your GitHub repo didn't work right away, and I have to fix those minor issues before seeing things in action.

      Author's profile photo Mio Yasutake
      Mio Yasutake
      Blog Post Author

      Hi Jhodel Cailan,

      Thanks for your nice comment !

      I've checked the Git repository and updated it.

      Author's profile photo Shruti Sangwan
      Shruti Sangwan

      Hi I am facing issue with .msg files. Does it support .msg ?

      Author's profile photo Willem PARDAENS
      Willem PARDAENS

      Thank you for your blog. It helped me implementing file upload in a CAP application.

      While your code is great for a 'Files' entity in the root of the project, I had to tweak it slightly for a 'Files' entity that is an association of a Parent entity (e.g. upload files for a specific object). In that case, the path of the Items in the UploadSet will be relational to the parent object ("path: 'Files' " instead of "path: '/Files' "), but this prohibits the use of the Refresh() command after a successful upload.

      The approach I took was to invoke a bound action on the parent object, with a sideEffect to refresh the associated Files entity. I'm just sharing the approach here in case others are looking for a solution to this.

      The controller:

      onUploadCompleted: function (oEvent) {
          const oUploadSet = Core.byId(uploadSetWidgetName);
          oUploadSet.removeAllIncompleteItems();
          // oUploadSet.getBinding("items").refresh(); //Fails
      
          this.base.editFlow.invokeAction("myService.refreshFiles", {
              contexts: this.getView().getBindingContext(),
          })
      }

      With the bound action specifying the sideEffects (the handler function is empty as we only need the side effect):

      @odata.draft.enabled
      entity Parent as projection on my.Parent
          actions {
              @cds.odata.bindingparameter.name  : '_it'
              @Common.SideEffects.TargetEntities: [ _it.Files ]
              action refreshFiles();
          }