Skip to main content
Version: Next

File Upload

Since Smartface uses NodeJS as backend, getting help from HTML forms to upload a file will not be helpful like it is done on the web applications.

Therefore, the upload process needs to be done with base64 conversions and relevant REST API calls.

tip

If you are the API provider, consider checking the maximum size supported for requests. Without any limit, the uploaded file size might exceed even Gigabytes of data.

Things to Consider While Developing File Upload on Mobile

Since the applications will be run on mobile devices, there are a few difference key aspects than web pages:

  • Network change or sudden network loss (user disabling network willingly or low signal)
  • Battery optimization. Especially on cellular connection, mobile devices will drain the battery since the data is sent from the device.
  • Background tasks and handling onMinimize and onMaximize events
    • iOS will not allow background tasks to linger when app is not active. On longer uploads, you should add relevant checks or warning to let user know that their process isn't complete.
    • Android will allow background tasks in certain conditions. Check the relevant Android guide in the link.
  • Size limitations

It is always good idea to check the size and shrink your Image or documents if the size is big enough. For videos, refer to Video Quality Optimization documentation:

Video Quality Optimization

Handling Network Change and Network Loss

API Reference: https://ref.smartface.io/interfaces/network.INetwork.html

import Network from "@smartface/native/device/network";

const notifier = new Network.createNotifier();
notifier.subscribe((connectionType) => {
console.log("Network change detected. New connection is : ", connectionType);
});

function upload() {
if (Network.connectionType === Network.ConnectionType.NONE) {
return; // No internet
} else if (Network.connectionType === Network.ConnectionType.MOBILE) {
// Warn the user about the cellular situation
alert(
"You are on cellular connection, continue to upload? Charges may apply."
);
}
// Your Upload code
}
info

It is always good idea to warn users before uploading via cellular connection. Most ISP have quotes or extra charges for upload processes, especially if the size is big.

The code above will return the status of the network, but it will not handle the cases like being connected to Wi-Fi and having an internet connection. To get status of internet connection, use the Network module at Extension Utility:

https://ref.smartface.io/beta/interfaces/network.INetwork.html#isConnected

Network.isConnected()

For advanced development, you could also consider listening to Network Change Events.

More information can be found under Network Documents:

Data & IO & NetworkNetwork

Handing Sizes of the Selected Files

If you have the File or Blob instance at your hands, determining the size is as simple as the API call like file.size or blob.size.

However, if you have a base64 at your disposal, we will resort to a simple trick to get its size.

Base64 strings have their paddings for each 64 bits, so we need to take that into account. More information about padding is located at the wikipedia document.

getFileSizeInMebiBytes(base64: string): number {
return base64.length * ( 3/4 ) * 1024 * 1024;
}

base64.length will return the character count of base64. We exclude the padding, then convert byte to mebibyte.

tip

Counter to contrary belief, multiplying by 1024 leads that unit to become e.g. Mebibyte instead of Megabyte. Megabyte is kilobyte multiplied by 1000. Get more information at this wikipedia article.

Example Page for Upload

Smartface has prepared a simple Page for the file upload purposes. You can check the code at Smartface Playground on GitHub:

scripts/pages/pgFileUpload.ts
import PgFileUploadDesign from "generated/pages/pgFileUpload";
import { Route, Router } from "@smartface/router";
import System from "@smartface/native/device/system";
import View from "@smartface/native/ui/view";
import Menu from "@smartface/native/ui/menu";
import MenuItem from "@smartface/native/ui/menuitem";
import Blob from "@smartface/native/global/blob";
import Image from "@smartface/native/ui/image";
import FileStream from "@smartface/native/io/filestream";
import File from "@smartface/native/io/file";
import Multimedia from "@smartface/native/device/multimedia";
import DocumentPicker from "@smartface/native/device/documentpicker";
import Network from "@smartface/native/device/network";

//You should create new Page from UI-Editor and extend with it.
export default class PgFileUpload extends PgFileUploadDesign {
protected uploadMenu = new Menu();
protected currentBase64 = "";
protected isUploading = false;
btnUpload: any;
aiUploadIndicator: StyleContextComponentType<View<any>>;
lblUploadIndicator: any;
flFileSelector: any;
lblFileSelectorName: any;
constructor(private router?: Router, private route?: Route) {
super({});
}

setVisible(view: StyleContextComponentType<View>, visible: boolean) {
view.dispatch({
type: "updateUserStyle",
userStyle: {
visible,
},
});
}

initUploadIndicator() {
this.setVisible(this.aiUploadIndicator, false);
}

selectFileAction(file: Image | File) {
if (file instanceof File) {
// If your video is supposed to be really big like >100 MiB, consider dividing them into chunks.
const fileStream = file.openStream(
FileStream.StreamType.READ,
FileStream.ContentMode.BINARY
);
if (fileStream.isReadable) {
this.lblFileSelectorName.text = file.name;
this.btnUpload.enabled = true;
const fileBlob = fileStream.readToEnd();
if (fileBlob instanceof Blob) {
// If the process is taking too much time, use async
this.currentBase64 = fileBlob.toBase64();
} else {
// Failed to read
this.currentBase64 = "";
}
}
} else if (file instanceof Image) {
this.lblFileSelectorName.text = "image.png";
this.btnUpload.enabled = true;
const imageBlob = file.toBlob();
this.currentBase64 = imageBlob.toBase64();
}
}

initMenu() {
this.uploadMenu.headerTitle = "Select a method to upload";
const menuItemCamera = new MenuItem({ title: "Take a Photo" });
const menuItemGallery = new MenuItem({
title: "Pick an Image From Gallery",
});
const menuItemDocument = new MenuItem({ title: "Pick a File" });
const menuItemCancel = new MenuItem({ title: "Cancel" });
menuItemCancel.ios.style = MenuItem.ios.Style.CANCEL;
menuItemCamera.onSelected = () => {
/**
* Don't forget to grant relevant permissions before calling this on your published app.
* Smartface Emulator will have these permissions.
*/
Multimedia.capturePhoto({
onSuccess: ({ image }) => this.selectFileAction(image),
page: this,
});
};
menuItemGallery.onSelected = () => {
Multimedia.pickFromGallery({
type: Multimedia.Type.IMAGE,
onSuccess: ({ image }) => this.selectFileAction(image),
page: this,
});
};
menuItemDocument.onSelected = () => {
DocumentPicker.pick({
type: [DocumentPicker.Types.ALLFILES],
onSuccess: (file) => this.selectFileAction(file),
onCancel: () => {},
onFailure: () => {},
page: this,
});
};
const menuItems = [menuItemGallery, menuItemCamera, menuItemDocument];
System.OS === System.OSType.IOS && menuItems.push(menuItemCancel); // Android doesn't need this
this.uploadMenu.items = menuItems;
this.flFileSelector.onTouchEnded = () => this.uploadMenu.show(this);
}

initButton() {
this.btnUpload.onPress = async () => {
if (this.isUploading === true) {
return; // Already uploading, don't do anything
}
await this.uploadFile(this.currentBase64);
};
}

async uploadFile(fileBas64: string) {
if (Network.connectionType === Network.ConnectionType.NONE) {
return; // No internet
} else if (Network.connectionType === Network.ConnectionType.MOBILE) {
// Warn the user about the cellular situation
alert(
"You are on cellular connection, continue to upload? Charges may apply."
);
}
if (this.isUploading === true) {
return; // Already uploading, don't do anything
}
this.toggleUpload(true);
/**
* Mocking the service call
*/
setTimeout(() => {
// Use the converted base64 to upload your file
console.info(fileBas64);
console.info("File size: ", this.getFileSizeInMebiBytes(fileBas64));
this.toggleUpload(false);
}, 2000);
}

toggleUpload(uploading: boolean) {
this.isUploading = uploading;
this.btnUpload.enabled = !uploading;
this.setVisible(this.aiUploadIndicator, uploading);
this.lblUploadIndicator.text = uploading
? "Uploading..."
: "Upload Complete.";
}

/**
* More info at: https://en.wikipedia.org/wiki/Base64#Output_Padding
*/
getFileSizeInMebiBytes(base64: string): number {
return base64.length * (3 / 4) * 1024 * 1024;
}

onShow() {
super.onShow();
this.headerBar.title = "File Upload";

}

onLoad() {
super.onLoad();
this.initMenu();
this.initButton();
this.initUploadIndicator();
}
}