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.
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.
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.
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:
- recuperiamo dalla request l’ID del file richiesto
- leggiamo dal db il nome del file e la cartella in cui è salvato
- costruiamo il path effettivo tramite il metodo getFSPathName (step 3 metodo 1)
- recuperiamo il file tramite fs.readFileSync
- 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:
- recuperiamo dalla request l’ID del file richiesto
- leggiamo dal db il nome del file e la cartella in cui è salvato
- costruiamo il path (quello effettivo verrà creato nel punto 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.