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

      14 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();
          }
      Author's profile photo Massimiliano Bucalo
      Massimiliano Bucalo

      Hello Willem,

      thank you for the solution.

      Can you share how you have implemented in the service.cds the refreshFiles() action for reading parent and associated entity to return and which component it is necessary to add in the Controller in order to not get "this.base.editFlow" undefined error message ?

      Also, I am facing problems in filtering the list of the files in the UploadSet items. Any kind of filter is not working (for examples /Files?$filter=ID eq 10) and all the files in the Files entity are always returned.

       

      Regards,

      Massimiliano.

      Author's profile photo Willem PARDAENS
      Willem PARDAENS

      Hi Massimiliano,

      The service handler is just an empty function, as the Side Effect will handle the UI refresh:

      srv.on('refreshFiles', req => { });

      For the controller you have to use a Controller Extension.

      App Manifest:

      "extends": {
          "extensions": {
              "sap.ui.controllerExtensions": {
                  "sap.fe.templates.ObjectPage.ObjectPageController": {
                      "controllerName": "myapp.ext.fileUpload"
                  }
              }
          }
      }

      Which allows you to extend the controller via the fileUpload.controller.js file:

      sap.ui.define(["sap/ui/core/mvc/ControllerExtension"], function (ControllerExtension) {
          "use strict";
          return ControllerExtension.extend("myapp.ext.fileUpload", {
              onUploadCompleted: function (oEvent) {
                  ...
              }
              ...
          }
      }

      Hope this helps.

      Author's profile photo Massimiliano Bucalo
      Massimiliano Bucalo

      Hello Willem,

      I solved by adding the ControllerExtensions, thank you.

      Also solved the filtering by implementing the List binding of the UploadSet "items" in the

      onBeforeRendering event of the Controller.

       

      Bye

      Author's profile photo Rohit Shukla
      Rohit Shukla

      Hello Willem PARDAENS. I am doing the same thing but getting issues while downloading the file from the ui5 app.

      I was trying the same within the ui5 application using oData v2 and I am using association in my cap service. I am able to successfully upload the file but while downloading it is giving me an empty value.  Below is the service after uploading the file. As you can see the media_src is providing me the attachment URL. When I try to open it, I am getting a blank screen in the browser. I am attaching the service and associated entities as well.

      Can you suggest what I am missing?

       

      attachmentService

      AssociatedEntities

      AssociatedEntities

      Author's profile photo Willem PARDAENS
      Willem PARDAENS

      Hi Rohit,

      Couple of points:

      • your SalesOrderPhotos is a 1-n composition of SalesOrders, yet it doesn't have a unique key?
      • double-check your upload is working. If you followed the blogs, the size is set in the _createEntity function, but yours shows 0
      •  as mentioned in the blog as well, in order to download the file you need to download it as a blob. Opening directly in the browser might not work.

      Hope this helps

      Author's profile photo Rohit Shukla
      Rohit Shukla

      Hello Willem,

      Thanks for the input. Below is my response

      • I have the unique key as a salesOrder. The composition is on salesOrderPhotos.salesOrder hence it considers salesOrder as a unique key. We can either use cuid common aspect or also like above. I also have salesOrderProducts assocaition and it works fine. While making a call I am passing salesOrder as a key and the call is a success. Crete entity looks like below( using sqlite as db hence i have to append key of child and root entity together.
      •   _createEntity: function (item, soNumber) {
                        var data = {
                            salesOrder_soNumber: soNumber,
                            fileName: item[0].getFileObject().name,
                            fileSize: item[0]._fFileSize,
                            imageType: item[0].getMediaType()
                        };
      • Regarding the size, I was using Integer as type and hence it was not showing up. I use double now and it works fine as shown below
      • While downloading I am getting blob object as undefined and I guess this might be the issue.
      • While saving attachment i see media_src is getting updated also. Just another thing I am running all this in local and using sqlite ad db

       

      Entites

      Entites

       

      Thanks

      Author's profile photo Willem PARDAENS
      Willem PARDAENS

      Hi Rohit,

      My point about the Key was that you only have a single key, which is the key of the parent. If you were to upload a second photo, this key will no longer be unique so it will fail. I suggest you add a UUID.

      The fileSize, ok I understand, though it is a bit weird that your size reads as 0.171 as this represents the number of bytes. Better would be to use ".getFileObject().size"

      Regarding the download, i'm using "item.getUrl()" to get the download path, though I see in your sample data the url property is not set.

      I suggest you take it step-by-step and do some debugging per line to see where your code deviates from the expectations, and then we can discuss that in specifc.

      Author's profile photo Rohit Shukla
      Rohit Shukla

      Sure Willem.
      Thanks a lot for your quick and  valuable input!