While using SAP Document Management Service, I have always struggled to write appropriate API call codes to upload and download files and folders.
A client library would facilitate me for interacting with the SAP Document Management Services (DMS) within my application. When integrating with SAP DMS, a dedicated client library becomes indispensable due to several crucial reasons.
In this blog I have tried to show you how to use a client library to connect and use SAP Document Management Service, also how to manage different tenant sessions and how to reuse them.
We are going to use a library called CMIS. This library is going to help us with the basic operation of the DMS , we are going to extend this library to add an additional capability.
npm install cmis
Once done you can refer this documentation for usage CMIS Documentation.
Other required libraries
const axios = require('axios').default;
const FormData = require('form-data');
const cmis = require("cmis");
const jwt = require('jsonwebtoken');
const mutexify = require('mutexify/promise')
Once you have created SDM you can create a service key or bind the service to an application to get the SDM credential.
The credentials looks like this.
{
"endpoints": {
"ecmservice": {
"url": "https://api-sdm-di.cfapps.<region>.hana.ondemand.com/"
},
"migrationservice": {
"url": "https://sdm-migration.cfapps.<region>.hana.ondemand.com"
}
},
...
"uaa": {
"clientid": "*****",
"clientsecret": "*****",
"url": "https://*****.authentication.<region>.hana.ondemand.com",
....
"credential-type": "instance-secret"
},
"uri": "https://api-sdm-di.cfapps.<region>.hana.ondemand.com/"
}
Let’s assume we have Session Manager (which is implemented below) that will keep track of all the session, and refreshes them from time to time and is tenant aware. It may look like this:
// const sdmCredentials = VCAP_SERVICES.sdm[0].credentials;
// create session manager
let sm = new CmisSessionManager(sdmCredentials);
let REPOSITORY_ID = "com.sap.sdm.test"
let tenantId = "provider";
// create repository
await sm.createRepositoryIfNotExists(REPOSITORY_ID, tenantId, {
"displayName": "Name of the repository",
"description": "Description for the repository",
"repositoryType": "internal",
"isVersionEnabled": "false",
"isVirusScanEnabled": "true",
"skipVirusScanForLargeFile": "false",
"isThumbnailEnabled": "false",
"isEncryptionEnabled": "false",
"hashAlgorithms": "SHA-256",
"isContentBridgeEnabled": "true",
"externalId": "sg.com.ncs.document.demo"
});
// get or create a new Session using the repo id and tenant id
// make sure to call this method everytime you perform a action on DMS, then the manager will refresh the session
let session = await sm.getOrCreateConnection(REPOSITORY_ID, "provider");
Now let us see how to use the CMIS session to create and read some documents.
let parentObj = await session.getObjectByPath("/");
const content = "dummy-content"; // string content
let result = await session.createDocument(parentObj.succinctProperties["cmis:objectId"], content, "test.txt");
console.log(result);
Creating document from string is very memory intensive, so let us create a document from stream, which is much less memory intensive.
let stream = fs.createReadStream("./test.txt");
let result = await session.createDocumentFromStream("/", stream, `test.txt`);
console.log(result);
let obj = await session.getObjectByPath("/test.txt");
let content = "appended-content";
let result = await session.appendContentFromString(obj.succinctProperties["cmis:objectId"], content, "test.txt");
console.log(result);
let obj = await session.getObjectByPath("/test.txt");
let stream = fs.createReadStream("./test-2.txt");
let result = await session.appendContentFromStream(obj.succinctProperties["cmis:objectId"], stream);
console.log(result);
let obj = await session.getObjectByPath("/abcd.txt");
let result = await session.getContentStream(obj.succinctProperties["cmis:objectId"]);
let writeStream = fs.createWriteStream("./abcd.txt");
result.body.pipe(writeStream);
let parentObj = await session.getObjectByPath("/");
let result = await session.createFolder(parentObj.succinctProperties["cmis:objectId"], "temp");
console.log(result);
let parentObj = await session.getObjectByPath("/");
let result = await session.createPath(parentObj.succinctProperties["cmis:objectId"], "/a/b/c/d");
console.log(result);
class CmisSessionManager {
tenantSessions = new Map()
lock = mutexify()
constructor(sdmCredentials) {
this.apiEndpoint = `${sdmCredentials.endpoints.ecmservice.url}browser`;
this.sdmCredentials = sdmCredentials;
}
async getOrCreateConnection(repositoryName, tenantId) {
const mapKey = `${tenantId}.${repositoryName}`;
// to improve performance, other tenants whose session are there should not wait in lock
if (!this.tenantSessions.has(mapKey) || this.isTokenExpired(this.tenantSessions.get(mapKey).getToken())) {
// allow only a single instance of this code to run
const release = await this.lock();
// check if session is created by some other blocked execution
if (!this.tenantSessions.has(mapKey) || this.isTokenExpired(this.tenantSessions.get(mapKey).getToken())) {
let token = await this._fetchJwtToken();
let session = new ExtendedCmisSession(this.apiEndpoint);
session.setToken(token);
await session.loadRepositories();
session.defaultRepository = session.repositories[repositoryName];
this.tenantSessions.set(mapKey, session);
console.log(`${mapKey} session created.`);
}
// release the lock
release();
}
return this.tenantSessions.get(mapKey);
}
async _fetchJwtToken() {
// This is to get the oauth token , which is used to create the folder ID
// implement your own as per requirement
const oauthUrl = this.sdmCredentials.uaa.url;
const oauthClient = this.sdmCredentials.uaa.clientid;
const oauthSecret = this.sdmCredentials.uaa.clientsecret;
const tokenUrl = oauthUrl + '/oauth/token?grant_type=client_credentials&response_type=token'
const config = {
headers: {
Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
}
}
const res = await axios.get(tokenUrl, config);
return res.data.access_token;
}
isTokenExpired(token) {
try {
const decodedToken = jwt.decode(token);
if (decodedToken && decodedToken.exp) {
const currentTimestamp = Math.floor(Date.now() / 1000); // Get the current time in seconds
return decodedToken.exp < currentTimestamp;
}
return true; // Token has no expiration information
} catch (error) {
return true; // Token decoding failed
}
}
async createRepositoryIfNotExists(repoId, tenantId, config) {
let token = await this._fetchJwtToken();
let session = new ExtendedCmisSession(this.apiEndpoint);
session.setToken(token);
await session.loadRepositories();
if (session.repositories.hasOwnProperty(repoId)) {
return;
}
config["externalId"] = repoId;
let axiosConf = {
method: 'post',
url: `${this.sdmCredentials.endpoints.ecmservice.url}rest/v2/repositories`,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
data: JSON.stringify({
"repository": config
})
};
const response = await axios.request(axiosConf);
return response.data;
}
}
class ExtendedCmisSession extends cmis.CmisSession {
getToken() {
return this.token;
}
async createPath(parentFolderId, path) {
const pathElements = path.split('/');
let currentFolderId = parentFolderId;
let createdFolder;
for (const folderName of pathElements) {
if (folderName) {
createdFolder = await this.createFolder(currentFolderId, folderName);
currentFolderId = createdFolder.succinctProperties['cmis:objectId'];
}
}
return createdFolder;
}
async createDocumentFromString(parentPath, content, filename) {
let parentObj = await this.getObjectByPath(parentPath);
return await session.createDocument(parentObj.succinctProperties["cmis:objectId"], content, filename);
}
async createDocumentFromStream(parentPath, content, filename) {
let data = new FormData();
data.append('cmisaction', 'createDocument');
data.append('succinct', 'true');
data.append('propertyId[0]', 'cmis:name');
data.append('propertyValue[0]', filename);
data.append('propertyId[1]', 'cmis:objectTypeId');
data.append('propertyValue[1]', 'cmis:document');
data.append('filename', filename);
data.append('_charset', 'UTF-8');
data.append('includeAllowableActions', 'true');
data.append('media', content);
const accessToken = this.token;
let config = {
method: 'post',
maxBodyLength: Infinity,
url: `${this.url}/${this.defaultRepository.repositoryId}/root${parentPath}`,
headers: {
'Authorization': 'Bearer ' + accessToken,
...data.getHeaders()
},
data: data
};
const res = await axios.request(config);
return res.data;
}
async appendContentFromString(objectId, content, filename) {
return await this.appendContentStream(objectId, content, false, filename);
}
async appendContentFromStream(objectId, contentStream, isLastChunk = false) {
let data = new FormData();
data.append('cmisaction', 'appendContent');
data.append('succinct', 'true');
data.append('isLastChunk', isLastChunk.toString());
data.append('objectId', objectId);
data.append('media', contentStream);
const accessToken = this.token;
let config = {
method: 'post',
maxBodyLength: Infinity,
url: `${this.url}/${this.defaultRepository.repositoryId}/root`,
headers: {
'Authorization': 'Bearer ' + accessToken,
...data.getHeaders()
},
data: data
};
const response = await axios.request(config);
return response.data;
}
}
In summary, a dedicated client library for SAP Document Management Services acts as a crucial middle-ware, simplifying integration, enhancing developer experience and ensuring robust and efficient utilization of DMS functionalities within an application.