In our previous NativeScript article, we previewed the Framework. For this article, let’s create a small demo project. To start with, let’s set up a scenario for the demo.
SimpleFileTransfer is a virtual file locker. Users can sign up for the service and get 100 MB of free virtual storage space. Users can then download and upload files on a server. Users will be able to increase their storage space by filling a form.
Let’s jot down the App functionalities before moving ahead:
You can find the whole code on GitHub.
The backend must provide the functionalities of managing routes, providing basic authentication and storage, and facilitating file transfers.
Based on the requirements above, we will be using the following stack:
We will also be using libraries like multer and bcrypt for specific functionalities that will be explained later on.
We will be using express-generator to set up the project. Install express-generator
globally using:
npm install express-generator -g
Start a new project using the command:
express file-server
Navigate to the file-server
directory and install the dependencies using npm install
. Also, install the following dependencies:
npm install multer async sequelize sqlite3 body-parser bcrypt --save
Additionally, we will be creating some extra folders for:
Sequelize is an ORM middleware for SQLite, MySQL, PostgreSQL and MSSQL. For small projects, it’s convenient to use the Sequelize + SQLite combo.
In our current scenario, we require only one Model. We will define our model user as follows:
const User = sequelize.define('user', {uid: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },username: { type: Sequelize.STRING, unique: true },password: Sequelize.STRING,quota: {type: Sequelize.INTEGER, defaultValue: 104857600},createdAt: Sequelize.DATE,updatedAt: Sequelize.DATE,})
We can use Sequelize’s Model.sync
to initialize Models Table in a database. To initialize users table, we will be using the code below.
User.sync({force: true}).then(() => {// Table created});
We will store the user model in the user.js
file in the model folder.
This part is pretty straightforward. For signup, the server accepts a username and password and stores it in the database. We will be using the bcrypt library to salt the passwords. As shown below, we are salting the password 10 times before storing it in the database. We are using Sequelize’s Model.create
to store the value. Once a user is created, we will create a directory on our server for his uploads.
The code is as below:
router.post('/', function(req, res, next) {console.log(req);bcrypt.hash(req.body.password, 10, function(err, hash) {User.create({ username: req.body.username, password: hash }).then(user => {if (!fs.existsSync('storage/'+user.get('uid'))){fs.mkdirSync('storage/'+user.get('uid'));}res.send({status: true, msg: 'User created', uid: user.get('uid')});}).catch(err => {res.send({status: false, msg: err });})});});
For login, the server accepts a username and password and validates it against the database. We are using Model.findAll
to get the database record. We use bcrypt.compare
to compare passwords.
router.post('/', function(req, res, next) {console.log(req);User.findAll({attributes: ["username", "password"],where: {username: req.body.username}}).then(dbQ => {if(dbQ.length > 0) {bcrypt.compare(req.body.password, dbQ[0].dataValues.password, function(err, result) {if (result == true){res.send({status: true, msg: 'Login Success'});} else {res.send({status: false, msg: 'Incorrect Password'});}});} else {res.send({status: false, msg: 'User not found'});}});});
An authenticated user is allowed to perform the following functions:
Let’s define the routes for those functions:
POST /users/:id/upload
GET /users/:id/download/:filename
GET /users/:id/details
POST /users/:id/increasequota
We will be using multer to handle uploads.
The multer library is useful to handle multi-part form data. Initially, we will be uploading the file to the uploads folder. Then, the file will be moved to /storage/uid
folder where uid is user id.
var storage = multer.diskStorage({destination: function (req, file, cb) {cb(null, 'uploads/')},filename: function (req, file, cb) {cb(null, file.originalname )}});
router.post('/:id/upload', upload.single('fileparam'), function(req, res, next) {if (!req.file) {console.log("No file received");return res.send({success: false,msg: "Error Uploading files"});} else {console.log('file received');fs.rename('uploads/'+ req.file.originalname, 'storage/'+req.params.id+'/'+req.file.originalname, function (err) {if (err) {console.log(err);return;}return res.send({success: true,msg: "File Uploaded"})});}});
The upload.single
method is used for handling uploads. This route is expecting a file with name fileparam
in the URL call. This is quickly done by adding a name attribute an HTML form. We will need the name attribute app side.
ExpressJS provides us with a function to set the download route, conveniently called download.
This is the logic we are following:
The code for the route is below
router.get('/:id/download/:filename', function(req, res, next) {const file = 'storage/'+req.params.id + '/' + req.params.filename;res.download(file);});
We will be invoking Model.update
to adjust the quota. By default, we have 104857600 bytes — which is equivalent to 100 MB — assigned to each user. You can find the query below.
router.post('/:id/increasequota', function(req, res, next) {User.update({quota: req.body.quota,}, {where: {uid: req.params.id}}).then(response => {res.send({status: true, msg: "Quota Increased"});}).catch(err => {res.send({status: false, msg: err});});});
This is a route which we will be using for fetching multiple data, such as:
/storage/userid
directory,We can fetch the storage limit of a user using Model.findAll
. For fetching file names and storage space, we are using fs .readdir
, fs.stat
and async
.
function getStorageSpace(relpath) {let space = 0;let fileNames = [];let promise = new Promise(function (resolve, reject) {fs.readdir(relpath, function (err, items) {if (err){reject(err);}fileNames = items;let filesArr = items.map(function (val) {return relpath + '/' + val;});async.map(filesArr, fs.stat, function (err, results) {
for (let i = 0; i < results.length; i++) {
if (err) {
reject(err);
}
space = space + results\[i\].size;
}
resolve({fileNames: fileNames, space: space });
});
});
});
return promise;
}
function getStorageLimit(uid){let promise = new Promise(function (resolve, reject) {User.findAll({attributes: ["quota"],where: {uid: uid}}).then(dbQ => {
if(dbQ.length < 1) {
reject("Quota Not Found")
} else {
resolve(dbQ\[0\].dataValues.quota);
}
}).catch(err => {
reject(err);
});
});
return promise;
}
router.get('/:id/details', function(req, res, next) {let it;let relpath = 'storage/'+req.params.id;Promise.all([getStorageSpace(relpath), getStorageLimit(req.params.id)]).then(result => {
res.send({storageLimit: result\[1\], occupiedSpace: result\[0\].space, fileNames: result\[0\].fileNames, remainingSpace: result\[1\]- result\[0\].space});
})
});
N.B.: The code works on assumption that user is not allowed to create a subdirectory in his folder.
The code for enforcing storage limit will be discussed later in the article.
For the app side, we will be taking an alternative approach. A demo project based on Angular-Blank template will be shared with users. A significant part of this article will cover details about plugins concerning the plugin functionalities.
We are consuming data from simple web services for Login / Signup / User Details Page.
As mentioned in the previous article, we can access these webservices using the HttpClient Module. The basic steps are as follows:
NativeScriptHttpClientModule
in the PageModule.HttpClient
and HttpHeaders
in Component or Provider.Content-Type
header to application/json
.For the JavaScript/TypeScript templates, we can use the NativeScript Core http module. The http. getJson
function provides the required framework to consume webservices. Alternatively, we can also use the fetch module.
As a response from the server, we will be receiving the uid
of a user. After authentication, we need to store the uid
so we can allow a mobile user to access /users/uid route
.
The NativeScript framework doesn’t have any method to store data persistently. We can add that functionality using plugins. We are going to look at two of these plugins.
nativescript-sqlite: This plugin provides an interface for the SQLite library. This works well if your app needs to store a large volume of records. Install it with:
tns plugin add nativescript-sqlite
nativescipt-localstorage: This plugins provides a key value API for string data, similar to window.localstorage
. This works well if your app doesn't have lot of records. Install it with:
tns plugin add nativescript-localstorage
The demo app will be using nativescript-localstorage
.
Let’s break this functionality into subtasks:
To choose a file and get a file path, we will be using the nativescript-mediapicker
plugin. The plugin has multiple modes and we can customize it for specific use cases. You can check the plugin documentation here.
To select a file, first, we need to define extensions. This is different for both OSes.
For Android devices, we have to use file extensions based on mime types like let extensions = ["xlsx", "xls", "doc", "docx", "ppt", "pptx", "pdf", "txt", "png"]
For iOS devices, we have to define extensions from list for Unified Type identifiers: let extensions = [kUTTypePDF, kUTTypeText];
You can read more about UTIs here and here.
The code for invoking filepicker
is as below:
let options: FilePickerOptions = {android: {extensions: extensions,maxNumberFiles: 1},ios: {extensions: extensions,multipleSelection: false}};
let mediafilepicker = new Mediafilepicker();mediafilepicker.openFilePicker(options);
`mediafilepicker.on("getFiles", function (res) {let results = res.object.get('results');console.dir(results);});
mediafilepicker.on("error", function (res) {let msg = res.object.get('msg');console.log(msg);});
mediafilepicker.on("cancel", function (res) {let msg = res.object.get('msg');console.log(msg);});`
As above, we will receive the filepath of a file in the getFiles
event.
We will send the file to the server using the nativescript-background-http
plugin. You can read about the plugin here.
Earlier, we defined the /users/:id/upload
route. As mentioned earlier, our server is expecting the file in the attribute named fileparam
.
The background http
provides us with two functions: uploadFile
and multipartUpload
. Since we need to set the name attribute, we will be using the multiPartUpload
function.
let session = bgHttp.session("image-upload");let request: bgHttp.Request = {url: Config.apiUrl + '/users/' + localStorage.getItem('uid') + '/upload' ,method: "POST",headers: {"Content-Type": "multipart/form-data"},description: 'FileName'};let params = [{name: 'file',filename: path}];let task: bgHttp.Task = session.multipartUpload(params, request);task.on("error", (e) => {reject(e);});task.on("complete", (e) => {resolve(e);});
We will be using the core file-system, platform and utils modules to achieve the result. Both Android and iOS handle downloads differently. We will be using isAndroid
and isIOS
variables from platform module to segregate the code.
The file-system module provides us with a knownFolders
sub-module. Three predefined folders for both Android and iOS are available:
knownFolders.currentApp()
knownFolders.documents()
knownFolders.temp()
Additionally, an iOS sub-module provides us with some other predefined folders. E.g:
knownFolders.ios.download
knownFolders.ios.sharedPublic
On an iOS scenario, this is straightforward:
utils.openFile
function to open the file.To download the files, we will be using the http module of the NativeScript framework. The getFile
function can be used to fetch files from the server and save them to a specific file location. The snippet for iOS is below:
let filePath: string = path.join(knownFolders.documents().path, fileName);getFile(download_url + fileName, filePath).then((resultFile) => {// The returned result will be File object}, (e) => {console.log(e);
Once the file has been downloaded, we can use the openFile
function from the utils
module to open a file on iOS.
The Android side of coding is a bit trickier. The locations of the knownFolders
module are as below.
/data/data/:appid/files/app
/data/user/:androiduser/:appid/files
/data/user/:androiduser/:appid/cache
As you can see, all the folders are located in /data
. /data
is inaccessible to normal users. Furthermore, external apps won't be able to access the files in those folders. Also, there is no openFile
function for Android.
As of now, the best we can do is:
To implement the functionality, we will be using a bit of native code. Before moving ahead, we will have to install tns-platform-declarations
with:
npm i tns-platform-declarations --save
Create a reference.d.ts
file in the root folder and add the following lines:
`/// <reference path="./node_modules/tns-platform-declarations/ios.d.ts" />``/// <reference path="./node_modules/tns-platform-declarations/android.d.ts" />`
You can check the readme for more details.
Android OS provides us with a function to access the external storage.
We will be using the constant DIRECTORY_DOWNLOADS
and the function getExternalStoragePublicDirectory
to create a publicly accessible download location.
We will append a path “SimpleFileTransfer” to create a custom folderPath
and filePath
.
const androidDownloadsPath = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS).toString();const androidFolderPath = fs.path.join(androidDownloadsPath, "SimpleFileTransfer");const filePath: string = fs.path.join(androidFolderPath, fileName);getFile(download_url + fileName, filePath).then((resultFile) => {// The returned result will be File object}, (e) => {console.log(e);
If you check your file explorer, a new directory will be created in the Downloads folder called SimpleFileTransfer. You will find all the files downloaded there.
We will be using the file-system
module. The Folder class of the file-system
module has a getEntities function which allows us to list files in a folder. As with fs.readdir
in Node.js, we can only list the files.
For iOS, the path is
const folderPath: string = fs.knownFolders.documents().path;
For Android, the path is
const androidDownloadsPath = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS).toString();
`const folderPath= fs.path.join(androidDownloadsPath, "SimpleFileTransfer");`
To access the Folder functions, we define a folder using
let internalFolder = fs.Folder.fromPath(folderPath);
Then, we use getEntities
to get a list of files:
internalFolder.getEntities().then((entities) => {// entities is array with the document's files and folders.
entities.forEach((entity) => {
let fileSize = fs.File.fromPath(entity.path).size;
this.listArray.push({
name: entity.name,
path: entity.path,
lastModified: entity.lastModified.toString(),
size : fileSize
});
});
}).catch((err) => {
// Failed to obtain folder's contents.
console.log(err.stack);
});
Additionally, we have used the size property of File class to get file size.
The storage limit can be enforced in two ways:
To enforce the former, we can modify the upload route as below:
Promise.all([getStorageSpace(relpath), getStorageLimit(req.params.id)]).then(result => {if (result[1] - result[0].space > req.file.size){fs.rename('uploads/'+ req.file.originalname, 'storage/'+req.params.id+'/'+req.file.originalname, function (err) {if (err) {return res.send({success: false,msg: "Error Uploading files"});}return res.send({success: true,msg: "File Uploaded"})});} else {return res.send({success: false,msg: "Storage Limit Exceeded"});}})
To enforce the latter, we get the file size of the file selected by the mediafilepicker
plugin and check it against the space remaining using the details webservice.
`let fileSize = fs.File.fromPath(results[0].file).size;`
if(fileSize < remainingSpace){// Send To server}`else {// alert user about lack of space}
This demo covers quite a few different concepts.
We divided the solution in a series of functionalities. We used core NativeScript for UX, interacting with the backend, file system management, and Routing. We extended the framework by installing plugins for functionalities like picking files. Going further, we used a bit of native code for solving a specific problem.
Using NativeScript allowed us to develop the app faster for both platforms as against individually.
If you want to learn how you can secure your NativeScript source code against client-side attacks, sign up for our free upcoming webinar on “Best Practices for Securing Your Mobile Apps”. Also be sure to check our tutorial.
Originally published at blog.jscrambler.com.