File-System Storage Service in HANA XS Advanced

In HANA XS Advanced, Le applicazioni vengono lanciate all’interno di un container che può essere interrotto in qualsiasi momento, quindi il file system ad esse collegato è effimero. Per risolvere questo problema, il sistema mette a disposizione il servizio File-System Storage (fs-storage) che garantisce la persistenza dei file in uno spazio condiviso da tutte le applicazioni ad esso collegate.

Scenario

In questo tutorial vedremo come implementare un servizio tramite il framework SAP Cloud Application Programming Model per la gestione del file system. Chiamando questo OData, sarà possibile leggere, creare e cancellare un file salvato nello spazio dedicato al servizio fs-storage.

 

Step 1 – creazione dell’istanza fs-storage

Per prima cosa, dobbiamo creare un’istanza del servizio fs-storage tramite il cockpit. Entriamo nello space desiderato e navighiamo verso il Service Marketplace tramite il menù sulla sinistra.

creazione dell’istanza fs-storage

Selezioniamo il servizio fs-storage e creiamo una nuova istanza. Per questo tutorial possiamo lasciare il service plan free e non abbiamo bisogno di specificare parametri aggiuntivi. Possiamo vedere la tabella contenente l’istanza appena creata.

fs-storage system

Step 2 – definizione del data model ed esposizione del servizio OData

Il contenuto dei file sarà salvato nel file system, mentre utilizzeremo una tabella definita tramite cds per salvare nel db un riferimento al file. Inseriamo questo codice nel data-model.cds

				
					namespace MediaModel;

entity Media {
    key ID		: UUID;
	
    @Core.MediaType: mediaType
    Content 	: LargeBinary;

    @Core.IsMediaType: true
    mediaType : String;
    FolderName	: String(50);
    FileName	: String(50);
}

				
			

Il campo ‘’Content’’ contiene il base64 del file e verrà svuotato prima del salvataggio del record su db. ‘’FolderName’’ serve per organizzare i file in cartelle e insieme a ‘’FileName’’ costituisce il percorso all’interno del file system in cui trovare il file salvato.

Spostiamoci nel file filesystem.cds sotto la cartella /srv e aggiungiamo questo codice per esporre un servizio odata (il path scelto è ‘’/filesystem’’) con un endpoint chiamato Media

				
					using { MediaModel } from '../db/data-model';
@path: 'filesystem'
service filesystemService {
    entity Media as projection on MediaModel.Media;
}
				
			

Step 3 – implementazione del servizio

Inseriamo la logica nel file filesystem.js. inizialmente, importiamo alcune librerie

				
					const cds = require("@sap/cds");
const xsenv = require("@sap/xsenv");
const path = require("path");
const fs = require("fs");
const { Readable } = require('stream')

				
			

‘’cds’’ è il framework che implementa il servizio OData, ‘’xsenv’’ ci consente di recuperare le informazioni sui servizi collegati, “path” fornisce le API per lavorare con file e cartelle, “fs” permette di gestire dati sul sistema operativo e ‘’Readable’’ ci servirà per mandare al client la response contenente il file.

Di seguito verranno descritti alcuni metodi di appoggio che utilizzeremo negli handler per l’endpoint Media.

Metodo 1: getFSPathName

				
					function getFSPathName(sFilepath) {
        var STORAGE_INSTANCE_NAME = 'kira_fs-storage';
        var servicesFS = xsenv.readServices();
        var services = xsenv.getServices({
            "fs-storage": {
                name: STORAGE_INSTANCE_NAME,
            },
        });
        var STORAGE_PATH_DIR = services["fs-storage"]["storage-path"];
        var oPathName = path.join(STORAGE_PATH_DIR, sFilepath);
        return oPathName;
    }

				
			

Questo metodo serve a costruire il path in cui sarà salvato il file. Tramite xsenv vengono recuperate le informazioni sulla configurazione del servizio fs-storage che abbiamo creato nello step 1. Il percorso creato corrisponde al path di base del fs-storage (STORAGE_PATH_DIR) + il path passato in input (sFilePath).

Metodo 2: createFSFolder

				
					function createFSFolder(sFolderName) {
    	try {
		  if (!fs.existsSync(sFolderName)) {
		  	console.log("Folder non esiste");
		    fs.mkdirSync(sFolderName);
		  }
		} catch (err) {
		  throw err;
		}
}

				
			

Questa funzione ci permette di creare una cartella all’interno del file system se non esiste.

Metodo 3: removeFSFile

				
					function removeFSFile(sFileName) {
    	var sPath = getFSPathName(sFileName)
		
    	fs.unlinkSync(sPath, (err) => {
		  if (err) {
			throw err;
		  }
		}) 
}

				
			

removeFSFile elimina un file salvato nel filesystem

Metodo 4: _createStream

				
					function _createStream (response) {        
        const buffer = new Buffer.from(response.split('base64').pop(), 'base64');
        const stream = new Readable()
        stream.push(buffer)
        stream.push(null)        

        return { value: stream }
    }

				
			

Questo metodo serve a costruire la response da inviare al client in fase di lettura. Inizialmente viene preso il base64 del file per poi creare un Readable. In questo modo il client può visualizzare direttamente l’allegato.

response

Step 3.1 – evento CREATE

Vediamo come creare un file all’interno del servizio fs-storage definendo un handler sull’evento “”on create Media””.

Nella request passeremo la cartella in cui vogliamo creare il file (FolderName), il nome del file (FileName) e il suo contenuto (Content). Inizialmente creiamo una cartella nel filesystem tramite il metodo createFSFolder (step 3 metodo 2) e creiamo il path dove verrà salvato il file tramite il metodo getFSPathName (step 3 metodo 1). Infine, scriviamo il file tramite fs.writeFileSync e cancelliamo il contenuto prima di salvare queste informazioni nel db (richiamando l’handler di default tramite next()).

				
					srv.on("CREATE", "Media", async (req, next) => {
    var sFolderName = req.data.FolderName;
    await createFSFolder(getFSPathName(sFolderName));
    var sFileName = req.data.FileName;
    var Base64 = req.data.Content;
    if (!sFolderName || !sFileName || !Base64){
        req.reject(404, 'Data not valid')
        return
    }
    var sPath = getFSPathName(path.join(sFolderName,sFileName));
    try{
    	await fs.writeFileSync(sPath, Base64, 'base64');
        req.data.Content = null;
			
        return next();
    }catch(err){
    	req.reply(err);
    }
});

				
			

Step 3.2 – evento READ

Nell’handler dell’evento “on READ Media” gestiamo sia la lettura di tutti i record, sia l’accesso in chiave alla singola risorsa. Nel primo caso, lasciamo agire l’handler di default del framework chiamando la funzione next(); nel secondo caso:

  1. recuperiamo dalla request l’ID del file richiesto
  2. leggiamo dal db il nome del file e la cartella in cui è salvato
  3. costruiamo il path effettivo tramite il metodo getFSPathName (step 3 metodo 1)
  4. recuperiamo il file tramite fs.readFileSync
  5. ritorniamo al client uno stream leggibile invocando il metodo _createStream (step 3 metodo 4)
				
					srv.on("READ", "Media", async (req, next) => {
	const url = req._.req.path;
	const id = req.params[0];
	
	if (!req.params[0]) {
		return next();
    }

	if (url.includes('Content')) {
		const Items = await cds.tx(req).run (
			SELECT.from('MediaModel.Media', oi => { oi.FolderName, oi.FileName }).where({ID:id})
		)
		if (!Items.length == 1) {
			req.reject(404, 'Media not found for the specified ID')
		}
		
		sFolderName = Items[0].FolderName;
		sFileName = Items[0].FileName;
		var sPath = getFSPathName(path.join(sFolderName, sFileName));
		try {
			var file = fs.readFileSync(sPath, "base64");
		} catch (err) {
			req.reject(404, 'File not found into filesystem')
		}
		
		return _createStream(file);
	} else {
		return next()
	}
});

				
			

Step 3.3 – evento DELETE

La prima parte della DELETE è molto simile a quella dell’handler per la READ:

  1. recuperiamo dalla request l’ID del file richiesto
  2. leggiamo dal db il nome del file e la cartella in cui è salvato
  3. costruiamo il path (quello effettivo verrà creato nel punto 4)
  4. cancelliamo il file tramite il metodo removeFSFile (step 3 metodo 3)


La DELETE va in errore se il file che si sta cercando di cancellare non esiste nel filesystem. Potrebbe succedere che, durante la creazione, il file non venga salvato nel fs-storage ma il suo puntamento nel db si. In questo caso si avrebbe un’inconsistenza nella base dati. Per gestire questa situazione, è stato inserito un parametro nelle request chiamato “forceDelete” che viene gestito nel catch e che permette di forzare la cancellazione del Media dal database.

				
					srv.on("DELETE", "Media", async (req, next) => {
    var id = req.data.ID;
	var forceDelete = req.req.query.forceDelete;
	const Items = await cds.tx(req).run (
	    SELECT.from('MediaModel.Media', oi => { oi.FolderName, oi.FileName}).where({ID:id})
	)
	if (!Items.length == 1) {
		req.reject(404, 'Media not found for the specified ID')
	}
	
	sFolderName = Items[0].FolderName;
	sFileName = Items[0].FileName;
	var sPath = path.join(sFolderName, sFileName);		
	try{
		await removeFSFile(sPath);
	    return next();
	}catch(err){
		console.log("Errore preso: " + err);
	if (forceDelete == true) {
		return next();
	}
		req.reject (418, err)
	}
});

				
			

Step 4 – configurazione dell’mta.yaml

Non ci resta che aggiungere il servizio fs-storage (di tipo com.sap.xs.fs) nell’mta e collegarlo al servizio node.

				
					ID: kira_fs
_schema-version: "2.1"
version: 0.0.4
modules:
  - name: kira_fs-db
    type: hdb
    path: db
    parameters:
      memory: 32M
      disk-quota: 512M
    requires:
      - name: kira_fs-db-hdi-container
  - name: kira_fs-srv
    type: nodejs
    path: srv
    parameters:
      memory: 64M
      disk-quota: 512M
    provides:
      - name: kira_fs-srv-ref
        public: true
        properties:
          url: ${default-url}
    requires:
      - name: kira_fs-db-hdi-container
      - name: kira_fs-storage
      - name: kira_fs-uaa

resources:
  - name: kira_fs-db-hdi-container
    type: com.sap.xs.hdi-container
    properties:
      hdi-container-name: ${service-name}
  - name: kira_fs-storage
    type: com.sap.xs.fs
  - name: kira_fs-uaa
    type: org.cloudfoundry.existing-service
    optional: true
    parameters:
      service-name: Kira-uaa

				
			

Vuoi maggiori informazioni?

Cerchi sviluppatori SAP a supporto delle tue attività professionali? 

Compila i campi qui sotto, ti ricontatteremo quanto prima.