本文转自:https://www.thepolyglotdeveloper.com/2017/04/build-image-manager-nativescript-node-js-minio-object-storage-cloud/

When building a mobile application, there are often scenarios where you need to storage files remotely and when I say files, I don’t mean database data. For example, maybe you want to develop an image manager or photo storage solution like what Facebook and Instagram offer? There are many solutions, for example you could store the files in your database as binary data, or you could store the files on the same server as your web application. However, there are better options, for example, you could use an object storage solution to store files uploaded from your mobile application. Popular object storage solutions include AWS S3 as well as the open source alternative Minio.

We’re going to see how to leverage Minio to store images that have been uploaded from an Android and iOS mobile application built with NativeScript and Angular.

Going into this you need to understand that we won’t be communicating directly to Minio via our mobile application. The Minio JavaScript client requires both an access key and secret key, both of which should never be stored in a client facing application. If someone were to reverse engineer your application and get these keys, your data would then be compromised. This means that we’ll be using NativeScript to communicate with a Node.js server that communicates with Minio.

Above is an animated image that explains the goal of our application. We’ll have a basic application that we can use to take pictures. After an image is captured it will be uploaded to our Node.js application which will upload it to Minio. Any image in our Minio bucket will be presented within the application. We’ll also have the ability to delete images as well.

The Requirements

To be successful with this tutorial, you’ll need the following prior to starting it:

If you have NativeScript installed, chances are you also have Node.js installed. Make sure that your versions meet the minimums that I’ve listed above. While you should have your own instance of Minio running, you can actually use the playground instance of Minio for free. However, don’t expect your data to live on forever and note that it is public.

Adding Features to the Node.js RESTful API

Not too long ago I wrote about using Minio in a Node.js API with Multer. To keep things easy, we’re going to use that example as a baseline and expand upon it. If you haven’t already, visit my previous tutorial, Upload Files to a Minio Object Storage Cloud with Node.js and Multer. While I recommend you read and understand what’s happening, you can download the code here. Note that the project you’re being provided with includes everything we plan to accomplish.

Everything we care about will be in the project’s app.js file. Before adding a few more features, it should have looked like this:

var Express = require("express");
var Multer = require("multer");
var Minio = require("minio");
var BodyParser = require("body-parser");
var app = Express(); app.use(BodyParser.json({limit: "4mb"})); var minioClient = new Minio.Client({
endPoint: 'play.minio.io',
port: 9000,
secure: true,
accessKey: 'Q3AM3UQ867SPQQA43P2F',
secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG'
}); app.post("/upload", Multer({storage: Multer.memoryStorage()}).single("upload"), function(request, response) {
minioClient.putObject("nraboy-test", request.file.originalname, request.file.buffer, function(error, etag) {
if(error) {
return console.log(error);
}
response.send(request.file);
});
}); app.post("/uploadfile", Multer({dest: "./uploads/"}).single("upload"), function(request, response) {
minioClient.fPutObject("nraboy-test", request.file.originalname, request.file.path, "application/octet-stream", function(error, etag) {
if(error) {
return console.log(error);
}
response.send(request.file);
});
}); app.get("/download", function(request, response) {
minioClient.getObject("nraboy-test", request.query.filename, function(error, stream) {
if(error) {
return response.status(500).send(error);
}
stream.pipe(response);
});
}); minioClient.bucketExists("nraboy-test", function(error) {
if(error) {
return console.log(error);
}
var server = app.listen(3000, function() {
console.log("Listening on port %s...", server.address().port);
});
});

The above code gives us a way to upload and download files via Node.js and Minio, but we don’t currently have a way to list files or remove files. Having these features are critical in our mobile application.

Take the following method for example:

app.delete("/delete", function(request, response) {
minioClient.removeObject("nraboy-test", request.query.filename, function(error) {
if(error) {
return response.status(500).send(error);
}
response.send({"deleted": true, "filename": request.query.filename});
});
});

The above method looks similar to what we’ve already seen, but this time it removes a file from a particular bucket based on its name.

So when it comes to listing files that exist in a bucket, we can include a method similar to this:

app.get("/list", function(request, response) {
var stream = minioClient.listObjects("nraboy-test");
var data = [];
stream.on("data", function(chunk) {
data.push(chunk);
});
stream.on("end", function() {
response.send(data);
});
});

Per the Minio documentation, listing involves working with a stream of data. Each response in the stream is one object. The above code allows us to take all objects in the stream, add them to an array, and return the array when it has closed.

Here is an example of what might come back in the response:

[
{
"name": "8b624dc4ee8dd33f3f78881e393cffc2.png",
"lastModified": "2017-04-05T17:09:33.380Z",
"etag": "a686f47a8618fd5873460015f65cf513",
"size": 679624
}
]

At this point we can run our Node.js web application. Take note that in the example we just saw, we are using the Minio playground instance, not one that I’m hosting myself. Feel free to use whatever you wish.

This brings us to the development of our mobile application with NativeScript.

Creating a NativeScript with Angular Image Manager Project

To start things off, we’re going to create a fresh NativeScript project that makes use of Angular. Using the NativeScript CLI, execute the following:

tns create minio-project --ng

The --ng flag indicates that we are creating an Angular project rather than a vanilla NativeScript project.

There will be two platform plugins used in this project that play a critical role towards its success. After taking a picture we’ll need to upload it. While we could do this with a standard HTTP request, it’s been known to have issues with larger files. For this reason we’ll be using the nativescript-background-http plugin. To install this plugin, execute the following:

tns plugin add nativescript-background-http

To prevent our application from keeping the user uninformed about slow tasks, we’re going to use Toast notifications to share things with the user. The nativescript-toast plugin can be installed by executing the following:

tns plugin add nativescript-toast

For more information on using Toast notifications, not discussed in this tutorial, check out a previous tutorial I wrote on the topic called, Display Toast Notifications in a NativeScript Angular Application.

Now there is a JavaScript plugin used in the application. This is not specific to NativeScript, but works fine because it is a JavaScript framework.

We won’t be manually naming the pictures captured in the application, but instead generating a name. This name will be an MD5 hash so we’ll need an appropriate plugin. From the command line, execute the following:

npm install blueimp-md5 --save

There are plenty of other options available, but this is the first that came to my mind.

At this point we can start developing our application.

Cleaning the Boilerplate Code in the NativeScript Template

Because we’re going to be creating a single page application, there is a lot of excess code that should be removed from the base template before continuing. This will help us prevent errors amongst other things.

Start by opening the project’s app/app.routing.ts file and make it look like the following:

import { NgModule } from "@angular/core";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { Routes } from "@angular/router"; const routes: Routes = []; @NgModule({
imports: [NativeScriptRouterModule.forRoot(routes)],
exports: [NativeScriptRouterModule]
})
export class AppRoutingModule {}

Since we are building a single page application, we want to remove all routing from the application. By default NativeScript will give you some item pages. The goal here is just to remove them.

Now head over to the project’s app/app.module.ts file and make it look like the following:

import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
import { NativeScriptHttpModule } from "nativescript-angular/http";
import { AppRoutingModule } from "./app.routing";
import { AppComponent } from "./app.component"; @NgModule({
bootstrap: [
AppComponent
],
imports: [
NativeScriptModule,
NativeScriptHttpModule,
AppRoutingModule
],
declarations: [
AppComponent
],
providers: [],
schemas: [
NO_ERRORS_SCHEMA
]
})
export class AppModule { }

Again, we’ve just removed the other page components that the NativeScript CLI had added for us when we started our project. Take note that in the above we’ve also added the NativeScriptHttpModule that will allow us to make HTTP requests to our API.

Building the Image Manager Component with Angular

Let’s focus on the core component in our application. It will have a stylesheet, HTML markup, and TypeScript logic for not only taking pictures, but also working with our Minio API.

You’re about to see a lot of code, but don’t worry, we’re going to break it down. Open the project’s app/app.component.ts file and include the following:

import { Component, OnInit, NgZone } from "@angular/core";
import { Http, Headers, RequestOptions } from "@angular/http";
import { Observable } from "rxjs/Observable";
import { ImageFormat } from "ui/enums";
import * as Camera from "camera";
import * as Toast from "nativescript-toast";
import "rxjs/Rx";
var FileSystem = require("file-system");
var BackgroundHttp = require("nativescript-background-http");
var MD5 = require("blueimp-md5"); @Component({
selector: "ns-app",
templateUrl: "app.component.html",
})
export class AppComponent implements OnInit { public images: Array<string>; public constructor(private http: Http, private zone: NgZone) {
this.images = [];
} public ngOnInit() {
this.list()
.subscribe(result => {
this.images = result;
});
} public takePicture() {
Camera.takePicture({saveToGallery: false, width: 320, height: 240}).then(picture => {
let folder = FileSystem.knownFolders.documents();
let path = FileSystem.path.join(folder.path, MD5(new Date()) + ".png");
picture.saveToFile(path, ImageFormat.png, 60);
this.upload("http://localhost:3000/upload", "upload", path)
.subscribe(result => {
this.zone.run(() => {
this.images.push(path.replace(/^.*[\\\/]/, ''));
});
}, error => {
console.dump(error);
});
});
} public upload(destination: string, filevar: string, filepath: string) {
return new Observable((observer: any) => {
let session = BackgroundHttp.session("file-upload");
let request = {
url: destination,
method: "POST"
};
let params = [{ "name": filevar, "filename": filepath, "mimeType": "image/png" }];
let task = session.multipartUpload(params, request);
task.on("complete", (event) => {
let file = FileSystem.File.fromPath(filepath);
file.remove().then(result => {
observer.next("Uploaded `" + filepath + "`");
observer.complete();
}, error => {
observer.error("Could not delete `" + filepath + "`");
});
});
task.on("error", event => {
console.dump(event);
observer.error("Could not upload `" + filepath + "`. " + event.eventName);
});
});
} public list(): Observable<any> {
return this.http.get("http://localhost:3000/list")
.map(result => result.json())
.map(result => result.filter(s => s.name.substr(s.name.length - 4) == ".png"))
.map(result => result.map(s => s.name));
} public remove(index: number) {
Toast.makeText("Removing image...").show();
this.http.delete("http://localhost:3000/delete?filename=" + this.images[index])
.map(result => result.json())
.subscribe(result => {
this.images.splice(index, 1);
});
} }

All application logic for this example will happen in the above.

After importing all the previously downloaded dependencies, we create a public variable for hold all the images obtained from the Minio Node.js API. By images I mean filenames, not actual image data.

The constructor method allows us to initialize this public variable as well as inject our Angular services for making HTTP requests and controlling the zone.

You should never load data in the constructor method, so instead we use the ngOnInit method to try to list the contents of our bucket. The list method called, looks like this:

public list(): Observable<any> {
return this.http.get("http://localhost:3000/list")
.map(result => result.json())
.map(result => result.filter(s => s.name.substr(s.name.length - 4) == ".png"))
.map(result => result.map(s => s.name));
}

Using RxJS we can make a request against our API and transform the results. The results are first converted to JSON, then we filter for only results where the filename has the PNG extension. All other items are removed from our result set. After transforming to return only PNG files, we do a transformation to only return the file names, rather than meta information about the files.

Jumping back up, we have the takePicture method:

public takePicture() {
Camera.takePicture({saveToGallery: false, width: 320, height: 240}).then(picture => {
let folder = FileSystem.knownFolders.documents();
let path = FileSystem.path.join(folder.path, MD5(new Date()) + ".png");
picture.saveToFile(path, ImageFormat.png, 60);
this.upload("http://localhost:3000/upload", "upload", path)
.subscribe(result => {
this.zone.run(() => {
this.images.push(path.replace(/^.*[\\\/]/, ''));
});
}, error => {
console.dump(error);
});
});
}

In this method we call the native device camera. After taking a picture, the picture is returned so we can manipulate it. For us, we want to upload it. To do this it must first be saved to the device filesystem. Since this example is simple, we generate a random filename based on the timestamp and save it to the correct path on the filesystem. After it is saved we can attempt to upload it using the HTTP plugin that was installed previously. If successful, the image filename is added to our list of images. This is done in a zone because of the alternative threads that we’re working with. The UI won’t update unless we’re in the correct zone. The plugin we’re using puts us in a different zone.

So what does uploading consist of? The upload method looks like the following:

public upload(destination: string, filevar: string, filepath: string) {
return new Observable((observer: any) => {
let session = BackgroundHttp.session("file-upload");
let request = {
url: destination,
method: "POST"
};
let params = [{ "name": filevar, "filename": filepath, "mimeType": "image/png" }];
let task = session.multipartUpload(params, request);
task.on("complete", (event) => {
let file = FileSystem.File.fromPath(filepath);
file.remove().then(result => {
observer.next("Uploaded `" + filepath + "`");
observer.complete();
}, error => {
observer.error("Could not delete `" + filepath + "`");
});
});
task.on("error", event => {
console.dump(event);
observer.error("Could not upload `" + filepath + "`. " + event.eventName);
});
});
}

We want to return an observable that can be subscribed to. This means we need to first define an upload request, provide the path to our file, then start a multipart upload. There are a few listener values as part of the HTTP plugin. When complete, we want to delete the file from our local device filesystem, and add a message to the data stream. If there is an error, we’ll push that to the stream instead.

Our final method is responsible for removing files:

public remove(index: number) {
Toast.makeText("Removing image...").show();
this.http.delete("http://localhost:3000/delete?filename=" + this.images[index])
.map(result => result.json())
.subscribe(result => {
this.images.splice(index, 1);
});
}

The above method is called during a press event so we’re passing in the index of the item that was pressed. We then show a Toast notification to say we’re going to be deleting a file.

When the API returns a response we can remove the image from the array which will remove it from the screen.

So what does the HTML behind this logic look like? Open the project’s app/app.component.html file and include the following HTML markup:

<ActionBar title="{N} Minio Application">
<ActionItem text="Capture" (tap)="takePicture()" ios.position="right"></ActionItem>
</ActionBar>
<ScrollView>
<FlexboxLayout flexWrap="wrap" flexDirection="row">
<StackLayout *ngFor="let image of images; let index = index" flexShrink="1" class="minio-image">
<Image src="http://localhost:3000/download?filename={{ image }}" (tap)="remove(index)"></Image>
</StackLayout>
</FlexboxLayout>
</ScrollView>

In the above example we have an action bar button that will call the takePicture method. In the core of the content we have a Flexbox that acts as a wrapping grid for our images. If images don’t fit on a row, they will be moved to the next row.

The FlexboxLayout is filled by looping through the array of images. They are downloaded on demand and displayed on the screen using the appropriate Node.js API endpoint. If any image is tapped, it will be removed.

The minio-image class name is custom and it was added to the app/app.css file like so:

@import 'nativescript-theme-core/css/core.light.css';

.minio-image {
height: 90;
margin: 5;
}

At this point the application should be ready to go!

Seeing the Application in Action

At this point you should have a Node.js API and NativeScript mobile application ready to go. If you decided not to walk through the tutorial, you can download all the code here.

Starting with the Node.js API, execute the following:

node app.js

If you didn’t already, you would have needed to download all the project dependencies first. With the API running, it should be available at http://localhost:3000.

Now jump into the NativeScript project and execute the following:

tns run ios --emulator

If you don’t have Xcode available, you can also use Android. Keep in mind that if you’re using Genymotion, you may need to change localhost in the API because VirtualBox operates a bit differently.

After uploading a file, you can find the file in Minio. If you’re using the playground like I was, you can check out https://play.minio.io:9000/minio/nraboy-test/.

Conclusion

You just saw how to upload files, more specifically images, in a NativeScript Android and iOS application to a Node.js API that is connected to a Minio object storage cloud.

It is useful to upload your media to object storage versus a database or application filesystem because with an object storage solution you get replication which protects your data from server failure. While you could replicate manually, it is far more convenient to use a solution that exists with a very nice set of SDKs.

[转]Build An Image Manager With NativeScript, Node.js, And The Minio Object Storage Cloud的更多相关文章

  1. Node.js NPM Tutorial: Create, Publish, Extend & Manage

    A module in Node.js is a logical encapsulation of code in a single unit. It's always a good programm ...

  2. Node.js npm 详解

    一.npm简介 安装npm请阅读我之前的文章Hello Node中npm安装那一部分,不过只介绍了linux平台,如果是其它平台,有前辈写了更加详细的介绍. npm的全称:Node Package M ...

  3. Node.js、express、mongodb 入门(基于easyui datagrid增删改查)

    前言 从在本机(win8.1)环境安装相关环境到做完这个demo大概不到两周时间,刚开始只是在本机安装环境并没有敲个Demo,从周末开始断断续续的想写一个,按照惯性思维就写一个增删改查吧,一方面是体验 ...

  4. 编写原生Node.js模块

    导语:当Javascript的性能需要优化,或者需要增强Javascript能力的时候,就需要依赖native模块来实现了. 应用场景 日常工作中,我们经常需要将原生的Node.js模块做为依赖并在项 ...

  5. 编写原生的Node.js模块

    导语:当Javascript的性能遭遇瓶颈,或者需要增强Javascript能力的时候,就需要依赖native模块来实现了. 应用场景 日常工作中,我们经常需要将原生的Node.js模块做为依赖并在项 ...

  6. 部署Node.js项目(CentOS)

    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,用来方便地搭建快速的易于扩展的网络应用.Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又 ...

  7. 阿里云部署Node.js项目(CentOS)

    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,用来方便地搭建快速的易于扩展的网络应用.Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又 ...

  8. 【转载】Centos系统采用NVM安装Node.js环境

    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,用来方便地搭建快速的易于扩展的网络应用.Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又 ...

  9. Practical Node.js摘录(2018版)第1,2章。

    大神的node书,免费 视频:https://node.university/courses/short-lectures/lectures/3949510 另一本书:全栈JavaScript,学习b ...

随机推荐

  1. STM32CubeMX+Keil裸机代码风格(2)

    10.找到STM32cubeMx所建的工程目录,在工程目录的同级目录下新建一个文件夹用来存放自己写的代码 11.用notepad++打开keil的工程文件,在这里的<Group>前面加上 ...

  2. 玩玩微信公众号Java版之七:自定义微信分享

    前面已经学会了微信网页授权,现在微信网页的功能也可以开展起来啦! 首先,我们先来学习一下分享,如何在自己的页面获取分享接口及让小伙伴来分享呢? 今天的主人公: 微信 JS-SDK, 对应官方链接为:微 ...

  3. EF学习笔记(七):读取关联数据

    总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 本篇参考原文链接:Reading Related Data 本章主要讲述加载显示关联数据: 数据加载分为以下三种 Lazy l ...

  4. Android-Java-接口Interface

    接口Interface 与 抽象类不同: 抽象类关注的是事物本质,例如:水果Fruit 属于抽象的,说去买水果 是模糊的概念 是抽象的概念 不具体,到底买什么水果不知道,而水果包含了 香蕉,橘子 很多 ...

  5. FFmpeg 学习(一):FFmpeg 简介

    一.FFmpeg 介绍 FFmpeg是一套可以用来记录.转换数字音频.视频,并能将其转化为流的开源计算机程序.采用LGPL或GPL许可证.它提供了录制.转换以及流化音视频的完整解决方案.它包含了非常先 ...

  6. Java学习笔记一:数据类型I

    GitHub代码练习地址:https://github.com/Neo-ML/JavaPractice/blob/master/IntPractice1.java https://github.com ...

  7. 使用HttpClient发送Get/Post请求 你get了吗?

    HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的.最新的.功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议 ...

  8. 一个致命的 Redis 命令,导致公司损失 400 万!!

    最近安全事故濒发啊,前几天发生了<顺丰高级运维工程师的删库事件>,今天又看到了 PHP 工程师在线执行了 Redis 危险命令导致某公司损失 400 万.. 什么样的 Redis 命令会有 ...

  9. netty入坑第一步:了解netty和编写简单的Echo服务器和客户端

    早期java API通过原生socket产生所谓的"blocking",大致过程是这样 这种的特点是每次只能处理一个请求,如果要实现多个请求并行,就还要分配一个新的线程来给每个客户 ...

  10. 剑指offer【05】- 用两个栈实现队列(java)

    题目:用两个栈实现队列 考点:栈和队列 题目描述:用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 解题思路:每次psuh是时先将stack2清空放入stck1(保 ...