import * as archiver from 'archiver';
import axios from 'axios';
import * as cheerio from 'cheerio';
import * as express from 'express';
import * as FormData from 'form-data';
import * as fs from 'fs';
import * as http from 'http';
import { networkInterfaces } from 'os';
import * as Path from 'path';
import * as QRCode from 'qrcode';
import * as request from 'request';
import { v4 as uuidv4 } from 'uuid';
import * as vscode from 'vscode';
export class ServerInstance {
private statusBarItem: vscode.StatusBarItem;
private outputChannel: vscode.OutputChannel;
private server: http.Server | undefined | null;
private portNumber: number | undefined | null;
private expressApp = express();
constructor() {
this.statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Right,
1
);
this.outputChannel = vscode.window.createOutputChannel(
'PayPaySimulatorServer'
);
}
start(portNumber: number, workspacePath: string) {
return new Promise((resolve, reject) => {
if (this.server) {
this.outputChannel.appendLine('Simulator already started');
reject(new Error('Simulator already started'));
} else if (!portNumber) {
this.outputChannel.appendLine('Simulator not started - Invalid Port');
reject(new Error('Simulator not started - Invalid Port'));
} else {
this.outputChannel.appendLine(
`Server is starting to listen to port ${portNumber}`
);
this.portNumber = portNumber;
const extPath = vscode.extensions.getExtension('PayPay.paypay-devtool')
?.extensionPath;
this.expressApp.use('/request/', (req, res) => {
req.pipe(request(req.url.substr(1))).pipe(res);
});
this.expressApp.use('/simulator', express.static(extPath + '/build'));
this.expressApp.get('/', function (_req, res) {
res.redirect('/simulator');
});
// WIP:
// this.expressApp.get('/notification-center', (req, res) => {
// const newUrl = req.query.url + '';
// req
// .pipe(
// request({
// url: newUrl,
// headers: {
// Authorization:
// 'Uct2BV0z6.....B3pV5LfA969BzcM4Q==',
// },
// })
// )
// .pipe(res);
// });
/*
* Package
*/
this.expressApp.use('/publish/package', async function (req, res) {
let archive = archiver('zip', { zlib: { level: 9 } });
//delete old zip
try {
if (fs.existsSync(workspacePath + '/app.zip')) {
fs.unlinkSync(workspacePath + '/app.zip');
}
} catch (err) {
vscode.window.showErrorMessage(err.message);
res.status(500).send({ error: err.message });
}
let output = await fs.createWriteStream(workspacePath + '/app.zip');
archive.on('error', function (err) {
vscode.window.showErrorMessage(err.message);
res.status(500).send({ error: err.message });
});
archive.pipe(output);
archive.append('', { name: 'app/' });
archive.glob(
'**',
{
cwd: workspacePath,
ignore: ['app.zip', '.*'],
},
{ prefix: 'app/' }
);
await archive.finalize();
res.send({ project: 'app.zip' });
});
/*
* Upload
*/
this.expressApp.use('/publish/upload', async function (req, res) {
try {
let zipData = fs.readFileSync(workspacePath + '/app.zip');
if (!zipData) {
return res.status(500).send({ error: 'Package not found' });
}
const form = new FormData();
form.append('file', zipData, { filename: 'app.zip' });
form.append('version', req.query.version);
form.append('activate', req.query.activate);
const uploadUrl =
'http://localhost:' +
portNumber +
'/request/https://stg.paypay-corp.co.jp/opa-mini-app/v1/files/upload?clientId=' +
req.query.clientID;
const formHeaders = form.getHeaders();
await axios
.post(uploadUrl, form, {
headers: {
...formHeaders,
'Content-Length': form.getLengthSync(),
Authorization: 'Bearer ' + req.query.serviceToken,
},
})
.then((response) => {
if (response.data.resultInfo.code !== 'SUCCESS') {
throw new Error('Unable to upload project');
}
})
.catch((err) => {
throw err;
});
} catch (err) {
return res.status(500).send({ error: err });
}
res.send();
});
this.expressApp.use('/network', function (req, res, next) {
try {
let addresses = [];
const interfaces = networkInterfaces();
for (var key in interfaces) {
let interfaceValues: String[] = [];
interfaces[key]?.forEach((element) => {
element['address'] && interfaceValues.push(element['address']);
});
addresses.push({ nic: key, values: interfaceValues });
}
res.send(JSON.stringify({ addresses: addresses }));
} catch (err) {
return res.status(500).send({ error: err });
}
res.send();
});
this.expressApp.use('/generateqrcode', async function (req, res) {
try {
const qrPath = extPath + '/tmp/MiniAppBarcode/';
if (!fs.existsSync(qrPath)) {
fs.mkdirSync(qrPath, { recursive: true });
}
const qrFilePath = qrPath + uuidv4() + '.png';
await QRCode.toFile(
qrFilePath,
req.query.data as string,
{ margin: 0 },
function (err) {
if (err) {
throw err;
}
}
);
res.send(JSON.stringify({ filePath: qrFilePath }));
} catch (err) {
return res.status(500).send({ error: err });
}
res.send();
});
///download files
this.expressApp.use('/downloadfile', async function (req, res) {
try {
const response = await axios({
method: 'GET',
url: req.query.url as string,
responseType: 'stream',
headers: req.query.header,
});
if (
Number(response?.headers['content-length']) >= 52428800 //50Mb in bytes
) {
return res.status(500).send({ error: 'SIZE_LIMIT_EXCEEDED' });
}
let filePath = req.query.filePath
? (req.query.filePath as string)
: '';
let filePathwithName = false;
const getFileName = function () {
let name = '';
let fileExt = response?.headers['content-type'].split('/')[1];
if (!filePath || filePath.charAt(filePath.length - 1) === '/') {
name = response?.request?.path?.substring(
response?.request?.path?.lastIndexOf('/') + 1
);
name = name?.split('?')[0];
if (name.indexOf('.') === -1) {
return name + '.' + fileExt;
}
} else if (filePath.indexOf('.') !== -1) {
filePathwithName = true;
return '';
} else {
filePathwithName = true;
return '.' + fileExt;
}
return name;
};
const dirPath =
workspacePath + '/files/' + filePath + getFileName();
let createFoldPath;
if (filePathwithName) {
let pth = filePath.substring(0, filePath.lastIndexOf('/') + 1);
createFoldPath = workspacePath + '/files/' + pth;
} else if (filePath.charAt(filePath.length - 1) === '/') {
createFoldPath = workspacePath + '/files/' + filePath;
} else {
createFoldPath = workspacePath + '/files/';
}
if (!fs.existsSync(createFoldPath)) {
fs.mkdirSync(createFoldPath, {
recursive: true,
});
}
let writer = fs.createWriteStream(dirPath);
response.data.pipe(writer);
writer.on('error', function (err) {
return res.status(500).send({ error: 'INVALID_PATH' });
});
res.send(
JSON.stringify(
req.query.filePath
? {
filePath: dirPath,
statusCode: 200,
}
: {
tempFilePath: dirPath,
statusCode: 200,
}
)
);
} catch (err) {
return res.status(500).send({ error: 'UNKNOWN' });
}
res.send();
});
this.expressApp.use('/getsavedfilelist', async function (req, res) {
try {
const getAllFiles = function (dirPath: any, arrayOfFiles: any) {
let files = fs.readdirSync(dirPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach(function (file) {
let fsState = fs.statSync(dirPath + '/' + file);
if (fsState.isDirectory()) {
arrayOfFiles = getAllFiles(
dirPath + '/' + file,
arrayOfFiles
);
} else {
// eslint-disable-next-line no-useless-escape
if (!/(^|\/)\.[^\/\.]/g.test(file)) {
arrayOfFiles.push({
filePath: Path.join(dirPath, '/', file),
size: fsState.size,
createTime: fsState.birthtimeMs,
});
}
}
});
return arrayOfFiles;
};
const fileList = getAllFiles(workspacePath + '/files/', []);
res.send(fileList);
} catch (err) {
return res.status(500).send({ error: err });
}
res.send();
});
this.expressApp.use('/removesavedfile', async function (req, res) {
try {
fs.unlinkSync(req.query.filePath as string);
res.send();
} catch (err) {
return res.status(500).send({ error: err });
}
res.send();
});
/*
* Workspace
*/
if (workspacePath.length) {
// TODO: improve detection of Vuejs projects
if (fs.existsSync(workspacePath + '/src/pages')) {
workspacePath += '/dist';
}
this.expressApp.use('/workspace', function (req, res, next) {
//json
try {
const jsonFile = workspacePath + req.path;
if (jsonFile.endsWith('.json')) {
if (jsonFile.includes('mock.json')) {
try {
const mockJson = JSON.parse(
fs.readFileSync(jsonFile).toString()
);
res.send(mockJson);
} catch (error) {
res.send('');
}
}
let pageJson = JSON.parse(fs.readFileSync(jsonFile).toString());
if (req.originalUrl.toLowerCase() === '/workspace/app.json') {
let defaultPageJson = {
window: {
navigationBarBackgroundColor: '#f7f7f7',
navigationBarTextStyle: 'black',
navigationBarTitleText: '',
navigationStyle: 'default',
backgroundColor: '#ffffff',
enablePullDownRefresh: false,
},
};
Object.assign(defaultPageJson.window, pageJson.window);
req.app.set('defaultPageJson', defaultPageJson);
Object.assign(pageJson, defaultPageJson);
}
res.send(pageJson);
}
} catch (err) {
res.send(req.app.get('defaultPageJson'));
}
//html
try {
let htmlFile = workspacePath + req.path;
if (!htmlFile.endsWith('.html')) {
htmlFile += '.html';
}
let html = fs.readFileSync(htmlFile);
let $ = cheerio.load(html);
$('body').prepend(``);
$('body').prepend(
''
);
if (
req.app.get('defaultPageJson').window?.enablePullDownRefresh
) {
$('body').append(`
`);
}
res.send($.html());
} catch {
next();
}
});
//default fallback
this.expressApp.use('/workspace', express.static(workspacePath));
} else {
this.outputChannel.appendLine(
'Simulator not started - No project selected'
);
return reject(
new Error('Simulator not started - No project selected')
);
/*app.use(
'/workspace',
express.static(extPath + '/build/workspace/empty')
);*/
}
this.outputChannel.show(true);
this.server = http
.createServer(this.expressApp)
.listen(portNumber, () => {
this.statusBarItem.command = 'paypay.devtool.simulator.openbrowser';
this.statusBarItem.tooltip = 'Click here to open in browser';
this.statusBarItem.text = `$(server) Simulator ${portNumber}`;
this.statusBarItem.show();
this.outputChannel.appendLine('Server started');
resolve();
})
.on('error', (err) => {
this.outputChannel.appendLine(
`Failed to start server due to ${err.message}`
);
reject(err);
this.server = null;
})
.on('request', (req, _res) => {
if (!req.originalUrl.startsWith('/simulator/')) {
this.outputChannel.appendLine(`${req.method} ${req.originalUrl}`);
}
});
}
});
}
stop() {
return new Promise((resolve, reject) => {
if (this.server) {
this.outputChannel.appendLine('Simulator is stopping');
this.server.close(() => {
this.server = null;
this.portNumber = null;
this.statusBarItem && this.statusBarItem.hide();
if (this.outputChannel) {
this.outputChannel.appendLine('Simulator stopped');
this.outputChannel.hide();
}
resolve();
});
} else {
this.outputChannel.appendLine('Simulator is not running');
reject(new Error('Simulator is not running'));
}
});
}
getUrl() {
return this.server ? 'http://localhost:' + this.portNumber : null;
}
dispose() {
this.stop();
this.statusBarItem.dispose();
this.outputChannel.dispose();
}
}