API Docs for: 2.0.0

src/node-itemloader.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014

/**
 * Main module loading content and creating applications
 *
 * @module server
 * @submodule itemLoader
 * @requires decompress-zip, gm, mime, request, ytdl-core, node-demux, node-exiftool, node-assets, node-utils, node-registry
 */

"use strict";

var fs           = require('fs');
var path         = require('path');
var url          = require('url');

var Unzip        = require('decompress-zip');
var gm           = require('gm');
var request      = require('request');
var ytdl         = require('ytdl-core');
var Videodemuxer = (process.arch !== 'arm') ? require('node-demux') : null;
var mv           = require('mv');
var sanitize     = require("sanitize-filename");

var exiftool     = require('../src/node-exiftool');        // gets exif tags for images
var assets       = require('../src/node-assets');          // asset management
var sageutils    = require('../src/node-utils');           // provides utility functions
var registry     = require('../src/node-registry');        // Registry Manager
var jsonfile     = require('jsonfile');
var cheerio     = require('cheerio');

var imageMagick;

/** don't use path.join for URL creation - all URLs use forward slashes **/


function AppLoader(publicDir, hostOrigin, config, imOptions, ffmpegOptions) {
	this.publicDir      = publicDir;
	this.hostOrigin     = hostOrigin;
	this.configuration  = config;
	this.displayWidth   = config.totalWidth;
	this.displayHeight  = config.totalHeight;
	this.titleBarHeight = (config.ui.auto_hide_ui === true) ? 0 : config.ui.titleBarHeight;

	imageMagick     = gm.subClass(imOptions);
	this.ffmpegPath = ffmpegOptions.appPath;
}


AppLoader.prototype.scaleAppToFitDisplay = function(appInstance) {
	var wallRatio   = this.displayWidth / (this.displayHeight - this.titleBarHeight);
	var iWidth      = appInstance.native_width;
	var iHeight     = appInstance.native_height;
	var aspectRatio = iWidth / iHeight;
	// Image wider than wall
	if (iWidth > (this.displayWidth - (2 * this.titleBarHeight)) && appInstance.aspect >= wallRatio) {
		// Image wider than wall
		iWidth  = this.displayWidth - (2 * this.titleBarHeight);
		iHeight = iWidth / appInstance.aspect;
	} else if (iHeight > (this.displayHeight - (3 * this.titleBarHeight)) && appInstance.aspect < wallRatio) {
		// Image taller than wall
		// Wall wider than image
		iHeight = this.displayHeight - (3 * this.titleBarHeight);
		iWidth  = iHeight * appInstance.aspect;
	}

	// Check against min dimensions
	if (iWidth < this.configuration.ui.minWindowWidth) {
		iWidth  = this.configuration.ui.minWindowWidth;
		iHeight = iWidth / aspectRatio;
	}
	if (iWidth > this.configuration.ui.maxWindowWidth) {
		iWidth  = this.configuration.ui.maxWindowWidth;
		iHeight = iWidth / aspectRatio;
	}
	if (iHeight < this.configuration.ui.minWindowHeight) {
		iHeight = this.configuration.ui.minWindowHeight;
		iWidth  = iHeight * aspectRatio;
	}
	if (iHeight > this.configuration.ui.maxWindowHeight) {
		iHeight = this.configuration.ui.maxWindowHeight;
		iWidth  = iHeight * aspectRatio;
	}

	appInstance.width  = iWidth;
	appInstance.height = iHeight;
};

AppLoader.prototype.loadImageFromURL = function(aUrl, mime_type, name, strictSSL, callback) {
	var _this = this;

	request({
		url: aUrl,
		encoding: null,
		strictSSL: strictSSL,
		agentOptions: {rejectUnauthorized: false, requestCert: false},
		headers: {'User-Agent': 'node'}},
	function(err1, response, body) {
		if (err1) {
			sageutils.log("Loader", "request error", err1);
			throw err1;
		}
		var localPath = path.join(_this.publicDir, "images", name);
		fs.writeFile(localPath, body, function(err2) {
			if (err2) {
				sageutils.log("Loader", "Error saving image:", aUrl, localPath);
			}

			assets.exifAsync([localPath], function(err3) {
				if (!err3) {
					_this.loadImageFromFile(localPath, mime_type, aUrl, aUrl, name, function(appInstance) {
						_this.scaleAppToFitDisplay(appInstance);
						appInstance.file = localPath;
						callback(appInstance);
					});
				}
			});

		});

	});
};

AppLoader.prototype.loadPdfFromURL = function(aUrl, mime_type, name, strictSSL, callback) {
	var localPath = path.join(this.publicDir, "pdfs", name);
	var _this = this;

	var tmp = fs.createWriteStream(localPath);
	tmp.on('error', function(err) {
		if (err) {
			throw err;
		}
	});
	tmp.on('close', function() {
		assets.exifAsync([localPath], function(err3) {
			if (!err3) {
				var appUrl = getSAGE2URL(localPath);
				var app_external_url = _this.hostOrigin + sageutils.encodeReservedURL(appUrl);

				_this.loadPdfFromFile(localPath, mime_type, appUrl, app_external_url, name, function(appInstance) {
					_this.scaleAppToFitDisplay(appInstance);
					appInstance.file = localPath;
					callback(appInstance);
				});
			}
		});
	});
	request({url: aUrl, strictSSL: strictSSL, headers: {'User-Agent': 'node'}}).pipe(tmp);
};

AppLoader.prototype.loadYoutubeFromURL = function(aUrl, callback) {
	var _this = this;

	ytdl.getInfo(aUrl, function(err, info) {
		if (err) {
			throw err;
		}

		var video = {index: -1, resolution: 0, type: ""};
		var audio = {index: -1, bitrate: 0, type: ""};
		for (var i = 0; i < info.formats.length; i++) {
			var type = info.formats[i].type.split(";")[0];
			if ((type === "video/mp4" || type === "video/webm") &&
				info.formats[i].resolution !== null &&
				info.formats[i].profile !== "3d") {
				// Compatible video file and not 3d
				var res = parseInt(info.formats[i].resolution.substring(0, info.formats[i].resolution.length - 1));
				if (res <= 1200 && res > video.resolution) {
					video.index = i;
					video.resolution = res;
					video.type = type;
				}
			} else if ((type === "audio/mp4" || type === "audio/webm")) {
				// or audio file
				var bitrate = info.formats[i].audioBitrate || 0;
				if ((audio.type === type && bitrate > audio.bitrate) ||
					(audio.type !== "audio/webm" && type === "audio/webm") ||
					(audio.type === "")) {
					audio.index = i;
					audio.bitrate = bitrate;
					audio.type = type;
				}
			}
		}

		_this.loadVideoFromURL(aUrl, "video/youtube", info.formats[video.index].url, info.title,
			function(appInstance, videohandle) {
				appInstance.data.video_url  = info.formats[video.index].url;
				appInstance.data.video_type = video.type;
				appInstance.data.audio_url  = info.formats[audio.index].url;
				appInstance.data.audio_type = audio.type;

				appInstance.file = aUrl;
				callback(appInstance, videohandle);
			});
	});
};

AppLoader.prototype.loadVideoFromURL = function(aUrl, mime_type, source_url, name, callback) {
	this.loadVideoFromFile(source_url, mime_type, aUrl, aUrl, name, callback);
};

AppLoader.prototype.loadImageFromDataBuffer = function(buffer, width, height, mime_type, aUrl,
	external_url, name, exif_data, callback) {

	// var source = buffer.toString("base64");
	var source = buffer;

	var aspectRatio = width / height;

	var metadata         = {};
	metadata.title       = "Image Viewer";
	metadata.version     = "1.0.0";
	metadata.description = "Image viewer for SAGE2";
	metadata.author      = "SAGE2";
	metadata.license     = "SAGE2-Software-License";
	metadata.keywords    = ["image", "picture", "viewer"];

	var appInstance = {
		id: null,
		title: name,
		application: "image_viewer",
		icon: buffer,
		type: mime_type,
		url: external_url,
		data: {
			img_url: source,
			type: mime_type,
			exif: exif_data,
			top: 0,
			showExif: false,
			crct: false
		},
		resrc: null,
		left: this.titleBarHeight,
		top: 1.5 * this.titleBarHeight,
		width: width,
		height: height,
		native_width: width,
		native_height: height,
		previous_left: null,
		previous_top: null,
		previous_width: null,
		previous_height: null,
		maximized: false,
		aspect: aspectRatio,
		animation: false,
		metadata: metadata,
		sticky: false,
		file: "",
		date: new Date()
	};
	this.scaleAppToFitDisplay(appInstance);
	callback(appInstance);
};

AppLoader.prototype.loadImageFromServer = function(width, height, mime_type, aUrl, external_url, name, exif_data, callback) {

	var aspectRatio = width / height;

	var metadata         = {};
	metadata.title       = "Image Viewer";
	metadata.version     = "1.0.0";
	metadata.description = "Image viewer for SAGE2";
	metadata.author      = "SAGE2";
	metadata.license     = "SAGE2-Software-License";
	metadata.keywords    = ["image", "picture", "viewer"];

	var appInstance = {
		id: null,
		title: name,
		application: "image_viewer",
		icon: exif_data ? exif_data.SAGE2thumbnail : null,
		type: mime_type,
		url: external_url,
		data: {
			img_url: external_url,
			type: mime_type,
			exif: exif_data,
			top: 0,
			showExif: false,
			crct: false
		},
		resrc: null,
		left: this.titleBarHeight,
		top: 1.5 * this.titleBarHeight,
		width: width,
		height: height,
		native_width: width,
		native_height: height,
		previous_left: null,
		previous_top: null,
		previous_width: null,
		previous_height: null,
		maximized: false,
		aspect: aspectRatio,
		animation: false,
		metadata: metadata,
		sticky: false,
		date: new Date()
	};
	this.scaleAppToFitDisplay(appInstance);
	callback(appInstance);
};

AppLoader.prototype.loadImageFromFile = function(file, mime_type, aUrl, external_url, name, callback) {
	var _this = this;

	if (mime_type === "image/jpeg" || mime_type === "image/png" || mime_type === "image/webp") {
		// Query the exif data
		var dims = assets.getDimensions(file);
		var exif = assets.getExifData(file);
		if (dims) {
			this.loadImageFromServer(dims.width, dims.height, mime_type, aUrl, external_url, exif.FileName, exif,
				function(appInstance) {
					appInstance.file = file;
					callback(appInstance);
				});
		} else {
			sageutils.log("Loader", "File not recognized", file, mime_type, aUrl);
		}

	} else if (mime_type === "image/svg+xml") {
		// SVG file
		var svgExif = assets.getExifData(file);
		if (svgExif) {
			var svgWidth  = parseInt(svgExif.ImageWidth,  10) || 600;
			var svgHeight = parseInt(svgExif.ImageHeight, 10) || 600;
			this.loadImageFromServer(svgWidth, svgHeight, mime_type, aUrl, external_url, svgExif.FileName, svgExif,
				function(appInstance) {
					appInstance.file = file;
					callback(appInstance);
				});
		} else {
			sageutils.log("Loader", "File not recognized:", file, mime_type, aUrl);
		}
	} else {
		var localPath = path.join(this.publicDir, "tmp", path.basename(name)) + ".png";
		var localUrl  = getSAGE2URL(localPath);

		imageMagick(file + "[0]").noProfile().bitdepth(8).flatten().setFormat("PNG").write(localPath, function(err, buffer) {
			if (err) {
				sageutils.log("Loader", "Error processing image file", file, localPath);
				return;
			}

			// Query the exif data
			var imgDims = assets.getDimensions(file);
			var imgExif = assets.getExifData(file);

			if (imgDims) {
				_this.loadImageFromServer(imgDims.width, imgDims.height, "image/png", localUrl,
					localUrl, name + ".png", imgExif, function(appInstance) {
						appInstance.file = localPath;
						callback(appInstance);
					}
				);
			} else {
				sageutils.log("Loader", "File not recognized:", file, mime_type, aUrl);
			}
		});

	}
};


AppLoader.prototype.loadVideoFromFile = function(file, mime_type, aUrl, external_url, name, callback) {
	// load video module, except on ARM processor (raspberry pi)
	if (process.arch !== 'arm') {
		var _this = this;
		var video = new Videodemuxer();
		video.on('metadata', function(data) {
			var metadata = {
				title: "Video Player", version: "2.0.0", description: "Video player for SAGE2",
				author: "SAGE2", license: "SAGE2-Software-License", keywords: ["video", "movie", "player"]
			};
			var exif = assets.getExifData(file);

			var stretch = data.display_aspect_ratio / (data.width / data.height);
			var native_width  = data.width * stretch;
			var native_height = data.height;

			var appInstance = {
				id: null,
				title: exif ? exif.FileName : name,
				application: "movie_player",
				icon: exif ? exif.SAGE2thumbnail : null,
				type: mime_type,
				url: external_url,
				data: {
					width: data.width,
					height: data.height,
					colorspace: "YUV420p",
					video_url: external_url,
					video_type: mime_type,
					audio_url: external_url,
					audio_type: mime_type,
					paused: true,
					frame: 0,
					numframes: data.num_frames,
					framerate: data.frame_rate,
					display_aspect_ratio: data.display_aspect_ratio,
					muted: false,
					looped: false,
					playAfterSeek: false
				},
				resrc: null,
				left:  _this.titleBarHeight,
				top:   1.5 * _this.titleBarHeight,
				width:  data.width,
				height: data.height,
				native_width:    native_width,
				native_height:   native_height,
				previous_left:   null,
				previous_top:    null,
				previous_width:  null,
				previous_height: null,
				maximized:       false,
				aspect:          native_width / native_height,
				animation:       false,
				metadata:        metadata,
				file:            file,
				date:            new Date()
			};
			_this.scaleAppToFitDisplay(appInstance);
			callback(appInstance, video);
		});
		video.load(file, {decodeFirstFrame: true, colorspace: "yuv420p"});
	}
};


AppLoader.prototype.loadPdfFromFile = function(file, mime_type, aUrl, external_url, name, callback) {
	// Assume default to Letter size
	var page_width  = 612;
	var page_height = 792;

	var aspectRatio = page_width / page_height;

	var metadata         = {};
	metadata.title       = "PDF Viewer";
	metadata.version     = "2.0.0";
	metadata.description = "PDF viewer for SAGE2";
	metadata.author      = "SAGE2";
	metadata.license     = "SAGE2-Software-License";
	metadata.keywords    = ["pdf", "document", "viewer"];

	var exif = assets.getExifData(file);

	var appInstance = {
		id: null,
		title: exif ? exif.FileName : name,
		application: "pdf_viewer",
		icon: exif ? exif.SAGE2thumbnail : null,
		type: mime_type,
		url: external_url,
		// V2 PDF viewer
		data: {
			doc_url: external_url,
			currentPage: 1,
			numberOfPageToShow: 1,
			horizontalOffset: 0,
			showingThumbnails: false
		},

		resrc:  null,
		left:   this.titleBarHeight,
		top:    1.5 * this.titleBarHeight,
		width:  page_width,
		height: page_height,
		native_width:    page_width,
		native_height:   page_height,
		previous_left:   null,
		previous_top:    null,
		previous_width:  null,
		previous_height: null,
		maximized: false,
		aspect:    aspectRatio,
		animation: false,
		metadata: metadata,
		sticky: false,
		file: file,
		date: new Date()
	};
	this.scaleAppToFitDisplay(appInstance);
	callback(appInstance);
};

AppLoader.prototype.loadNoteFromFile = function(file, mime_type, aUrl, external_url, name, callback) {
	// Find the app. Look it the file name in the registry. Get path, navigate to the path's instruction.json file.
	var appName = registry.getDefaultApp(file);
	var localPath = getSAGE2Path(appName);
	var instructionsFile = path.join(localPath, "instructions.json");

	// Will read the instruction file and then launch app with instructionfile parameters.
	var _this = this;
	fs.readFile(instructionsFile, 'utf8', function(err, json_str) {
		if (err) {
			sageutils.log("Loader", "cannot read application file", instructionsFile);
			return;
		}
		var appUrl = getSAGE2URL(localPath);
		var app_external_url = _this.hostOrigin + sageutils.encodeReservedURL(appUrl);
		var appInstance = _this.readInstructionsFile(json_str, localPath, mime_type, app_external_url);
		appInstance.data.file = assets.getURL(file);
		appInstance.file = file;

		// This will add the contents of the note to the send data values. Assuming the var is unique.
		appInstance.data.contentsOfNoteFile = fs.readFileSync(file, 'utf8');
		callback(appInstance);
	});
};


AppLoader.prototype.loadDoodleFromFile = function(file, mime_type, aUrl, external_url, name, callback) {
	// Find the app. Look it the file name in the registry. Get path, navigate to the path's instruction.json file.
	var appName = registry.getDefaultApp(file);
	var localPath = getSAGE2Path(appName);
	var instructionsFile = path.join(localPath, "instructions.json");

	// Will read the instruction file and then launch app with instructionfile parameters.
	var _this = this;
	fs.readFile(instructionsFile, 'utf8', function(err, json_str) {
		if (err) {
			sageutils.log("Loader", "cannot read application file", instructionsFile);
			return;
		}
		var appUrl = getSAGE2URL(localPath);
		var app_external_url = _this.hostOrigin + sageutils.encodeReservedURL(appUrl);
		var appInstance = _this.readInstructionsFile(json_str, localPath, mime_type, app_external_url);
		appInstance.data.file = assets.getURL(file);
		appInstance.file = file;

		// Making sure the file exist first
		if (sageutils.fileExists(file)) {
			var content = fs.readFileSync(file).toString('base64');
			// This will add the contents of the note to the send data values.
			// Assuming the var is unique.
			appInstance.data.contentsOfDoodleFile = "data:image/png;base64," + content;
		} else {
			appInstance.data.contentsOfDoodleFile = null;
		}

		// Include the file name to reset to original
		var fbasic = file;
		while (fbasic.indexOf("/") > -1) {
			fbasic = fbasic.substring(fbasic.indexOf("/") + 1);
		}
		while (fbasic.indexOf("\\") > -1) {
			fbasic = fbasic.substring(fbasic.indexOf("\\") + 1);
		}
		fbasic = fbasic.substring(0, fbasic.indexOf(".doodle"));
		appInstance.data.fileName = fbasic;
		callback(appInstance);
	});
};


AppLoader.prototype.loadAppFromFileFromRegistry = function(file, mime_type, aUrl, external_url, name, callback) {
	// Find the app!!
	var appName = registry.getDefaultApp(file);
	var localPath = getSAGE2Path(appName);
	var instructionsFile = path.join(localPath, "instructions.json");

	var _this = this;
	fs.readFile(instructionsFile, 'utf8', function(err, json_str) {
		if (err) {
			sageutils.log("Loader", "cannot read application file", instructionsFile);
			return;
		}

		var appUrl = getSAGE2URL(localPath);
		var app_external_url = _this.hostOrigin + sageutils.encodeReservedURL(appUrl);

		var appInstance = _this.readInstructionsFile(json_str, localPath, mime_type, app_external_url);
		appInstance.data.file = assets.getURL(file);
		// Seems to cause issues, when drag-drop apps
		// appInstance.file = file;
		callback(appInstance);
	});
};

AppLoader.prototype.loadAppFromFile = function(file, mime_type, aUrl, external_url, name, data, callback) {
	var _this = this;
	var zipFolder = file;

	var instructionsFile = path.join(zipFolder, "instructions.json");
	fs.readFile(instructionsFile, 'utf8', function(err, json_str) {
		if (err) {
			throw err;
		}

		var appInstance = _this.readInstructionsFile(json_str, zipFolder, mime_type, external_url);
		sageutils.mergeObjects(data, appInstance.data);

		_this.scaleAppToFitDisplay(appInstance);
		appInstance.file = file;
		callback(appInstance);
	});
};

AppLoader.prototype.loadZipAppFromFile = function(file, mime_type, aUrl, external_url, name, callback) {
	var _this = this;
	var zipFolder = path.join(path.dirname(file), name);

	var unzipper = new Unzip(file);
	unzipper.on('extract', function(log) {
		var instuctionsFile = path.join(zipFolder, "instructions.json");
		var hasInstructionsFile = fs.existsSync(instuctionsFile);

		// Check if UnityLoader.js exists in unzipped directory structure
		var unityLoader = _this.existsInDir(zipFolder, "UnityLoader.js");
		if (unityLoader) {
			var instructionInfo = {
				instuctionsFile: instuctionsFile,
				instructionsExists: hasInstructionsFile,
				mime_type: mime_type,
				external_url: external_url
			};
			sageutils.log("AppLoader", "Found Unity app", unityLoader);
			_this.loadUnityAppFromZip(_this, unityLoader, zipFolder, instructionInfo, callback);
		} else {
			fs.readFile(instuctionsFile, 'utf8', function(err1, json_str) {
				if (err1) {
					throw err1;
				}

				assets.exifAsync([zipFolder], function(err2) {
					if (err2) {
						throw err2;
					}

					var appInstance = _this.readInstructionsFile(json_str, zipFolder, mime_type, external_url);
					_this.scaleAppToFitDisplay(appInstance);
					// Seems to cause issues, when drag-drop, the first time the app is opened.
					// appInstance.file = file;
					callback(appInstance);
				});
			});
		}

		// delete original zip file
		fs.unlink(file, function(err) {
			if (err) {
				throw err;
			}
		});
	});
	unzipper.extract({
		path: path.dirname(file),
		filter: function(extractedFile) {
			if (extractedFile.type === "SymbolicLink") {
				return false;
			}
			if (extractedFile.filename === "__MACOSX") {
				return false;
			}
			if (extractedFile.filename.substring(0, 1) === ".") {
				return false;
			}
			if (extractedFile.parent.length >= 8 && extractedFile.parent.substring(0, 8) === "__MACOSX") {
				return false;
			}

			return true;
		}
	});
};

AppLoader.prototype.loadUnityAppFromZip = function(appLoader, unityLoader, zipFolder, data, callback) {
	var unityIndexHtml = path.join(zipFolder, "index.html");
	sageutils.log('AppLoader', 'Unity index file', unityIndexHtml);

	fs.readFile(unityIndexHtml, 'utf8', function(err1, html_str) {
		if (err1) {
			throw err1;
		}

		// Read index.html
		var indexHtml = cheerio.load(html_str);

		// Grab html info
		var htmlTitle = indexHtml("title").text();
		var htmlCanvasHeight = indexHtml("canvas").attr("height");
		var htmlCanvasWidth = indexHtml("canvas").attr("width");

		// Unity 5.6+ no longer stores the width/height in <canvas>
		if (htmlCanvasHeight == undefined) {
			// Get width/height from div style, parse out ; and spaces
			var style = indexHtml("div").attr("style").split(/[\s;]+/);
			htmlCanvasHeight = style[3];
			htmlCanvasWidth = style[1];
		}

		// Title is in the form 'Unity WebGL Player | [Product Name]'
		// Get just the Product Name
		htmlTitle = htmlTitle.split("|")[1].trim();

		// Set the html <title> the new parsed name
		indexHtml("title").text(htmlTitle);

		// Get the canvas width/height, parsing out "px"
		htmlCanvasHeight = parseInt(htmlCanvasHeight.slice(0, htmlCanvasHeight.indexOf("p")));
		htmlCanvasWidth  = parseInt(htmlCanvasWidth.slice(0, htmlCanvasWidth.indexOf("p")));

		// Sets style flag for proper SAGE2 scaling
		var htmlStyle = "* {margin: 0; padding: 0;}html, body {width: 100vw;height: 100vh;}";
		htmlStyle += "canvas {height: 100vh;width: 100vw;display: block;}";

		// If <style> tag exists, set it. If not, append it to head (Unity 5.6+)
		if (indexHtml("style").length == 1) {
			indexHtml("style").text(htmlStyle);
		} else {
			indexHtml('head').append("<style>" + htmlStyle + "</style>");
		}

		// Update index.html
		fs.writeFile(unityIndexHtml, indexHtml.html(), function(err) {
			if (err) {
				sageutils.log('AppLoader', 'Unity error writing file', err);
				return;
			}
		});

		// Generate instructions.json if it doesn't exist
		if (data.instructionsExists == false) {
			var obj = {
				main_script: "UnityLoader.js",
				icon: "",
				width: htmlCanvasWidth,
				height: htmlCanvasHeight,
				resize: "free",
				animation: true,
				dependencies: [],
				load: {},
				title: htmlTitle,
				version: "1.0.0",
				description: "Loads a Unity3D webgl output into a webview",
				keywords: ["sage2", "unity3d", "webview"],
				author: "",
				license: "SAGE2-Software-License"
			};
			jsonfile.writeFileSync(data.instuctionsFile, obj);
		}

		fs.readFile(data.instuctionsFile, 'utf8', function(err1, json_str) {
			if (err1) {
				throw err1;
			}

			assets.exifAsync([zipFolder], function(err2) {
				if (err2) {
					throw err2;
				}
				sageutils.log("Loader", "Found unity file", unityLoader);
				var appInstance = appLoader.readInstructionsFile(json_str, zipFolder, data.mime_type, data.external_url);
				appLoader.scaleAppToFitDisplay(appInstance);
				// Seems to cause issues, when drag-drop, the first time the app is opened.
				// appInstance.file = file;
				callback(appInstance);
			});
		});
	});
};

AppLoader.prototype.createMediaStream = function(source, type, encoding, name, color, width, height, callback) {
	var aspectRatio = width / height;

	var metadata         = {};
	metadata.title       = "Stream Player";
	metadata.version     = "1.0.0";
	metadata.description = "Stream player for SAGE2";
	metadata.author      = "SAGE2";
	metadata.license     = "SAGE2-Software-License";
	metadata.keywords    = ["stream", "network", "player"];

	var appInstance = {
		id: null,
		title: name,
		color: color,
		application: "media_stream",
		type: "application/stream",
		url: null,
		data: {
			src: source,
			type: type,
			encoding: encoding
		},
		resrc: null,
		left: this.titleBarHeight,
		top: 1.5 * this.titleBarHeight,
		width: width,
		height: height,
		native_width: width,
		native_height: height,
		previous_left: null,
		previous_top: null,
		previous_width: null,
		previous_height: null,
		maximized: false,
		aspect: aspectRatio,
		animation: false,
		sticky: false,
		metadata: metadata,
		file: "",
		date: new Date()
	};
	this.scaleAppToFitDisplay(appInstance);
	callback(appInstance);
};


AppLoader.prototype.createMediaBlockStream = function(name, color, colorspace, width, height, callback) {
	var aspectRatio = width / height;

	var metadata         = {};
	metadata.title       = "Stream Player";
	metadata.version     = "1.0.0";
	metadata.description = "Stream player for SAGE2";
	metadata.author      = "SAGE2";
	metadata.license     = "SAGE2-Software-License";
	metadata.keywords    = ["stream", "network", "player"];

	var appInstance = {
		id: null,
		title: name,
		color: color,
		application: "media_block_stream",
		type: "application/stream",
		url: null,
		data: {
			colorspace: colorspace || "YUV420p",
			width: width,
			height: height
		},
		resrc: null,
		left: this.titleBarHeight,
		top: 1.5 * this.titleBarHeight,
		width: width,
		height: height,
		native_width: width,
		native_height: height,
		previous_left: null,
		previous_top: null,
		previous_width: null,
		previous_height: null,
		maximized: false,
		aspect: aspectRatio,
		animation: false,
		sticky: false,
		metadata: metadata,
		file: "",
		date: new Date()
	};
	this.scaleAppToFitDisplay(appInstance);
	callback(appInstance);
};

AppLoader.prototype.loadApplicationFromRemoteServer = function(application, callback) {
	this.loadApplication({location: "remote", application: application}, callback);
};

AppLoader.prototype.loadFileFromWebURL = function(file, callback) {
	// Add the URL in the asset DB
	var appIcon = path.resolve('public', 'images', 'link_256.png');
	var urlName = file.url;
	// build a fake EXIF structure
	var exif = {FileName: urlName, icon: appIcon, MIMEType: "sage2/url",
		FileSize: 0, FileDate: new Date(),
		SAGE2user: file.SAGE2_ptrName, SAGE2color: file.SAGE2_ptrColor,
		metadata: {}};
	// Store the asset
	assets.addURL(urlName, exif);
	assets.saveAssets();

	// Extract the filename from URL,
	// used in case of dragging an image or PDF for instance from the web
	var mime_type = file.type;
	var filename = decodeURI(file.url.substring(file.url.lastIndexOf("/") + 1));

	// Load the app
	this.loadApplication({location: "url", url: file.url, type: mime_type, name: filename, strictSSL: false},
		callback);
};

AppLoader.prototype.createJupyterApp = function(source, type, encoding, name, color, width, height, callback) {
	var aspectRatio = width / height;

	var metadata         = {};
	metadata.title       = "Jupyter";
	metadata.version     = "1.0.0";
	metadata.description = "JupyterLab-SAGE2 Application";
	metadata.author      = "SAGE2";
	metadata.license     = "SAGE2-Software-License";
	metadata.keywords    = ["jupyter", "jupyterlab"];

	var appInstance = {
		id: null,
		title: name,
		color: color,
		application: "JupyterLab",
		icon: "/images/jupyter.png",
		type: "mime_type",
		url: "src",
		data: {
			src: source,
			type: type,
			encoding: encoding
		},
		load: {
			imgDict: {},
			mainImgs: {},
			page: 1
		},
		resrc: null,
		left: this.titleBarHeight,
		top: 1.5 * this.titleBarHeight,
		width: width,
		height: height,
		native_width: width,
		native_height: height,
		previous_left: null,
		previous_top: null,
		previous_width: null,
		previous_height: null,
		maximized: false,
		aspect: aspectRatio,
		resizeMode: "free",
		animation: false,
		sticky: false,
		metadata: metadata,
		date: new Date()
	};
	this.scaleAppToFitDisplay(appInstance);
	callback(appInstance);
};


function getSAGE2Path(getName) {
	// pathname: result of the search
	var pathname = null;
	// walk through the list of folders
	for (var f in global.mediaFolders) {
		// Get the folder object
		var folder = global.mediaFolders[f];
		// Look for the folder url in the request
		var pubdir = getName.split(folder.url);
		if (pubdir.length === 2) {
			// convert the URL into a path
			var suburl = path.join('.', pubdir[1]);
			// pathname   = url.resolve(folder.path, suburl);
			pathname = path.resolve(folder.path, suburl);
			pathname   = decodeURIComponent(pathname);
			break;
		}
	}
	// if everything fails, look in the default public folder
	if (!pathname) {
		pathname = getName;
	}
	return pathname;
}

function getSAGE2URL(getName) {
	var filename = path.resolve(getName);
	// Calculate a SAGE2 URL based on the full pathname
	var sage2URL = "";
	for (var f in global.mediaFolders) {
		var folder = global.mediaFolders[f];
		var up;
		up = path.resolve(folder.path);
		var pubdir = filename.split(up);
		if (pubdir.length === 2) {
			sage2URL = sageutils.encodeReservedURL(folder.url + pubdir[1]);
			return sage2URL;
		}
	}
	return sage2URL;
}


AppLoader.prototype.loadFileFromLocalStorage = function(file, callback) {
	var mime_type;
	var localPath = getSAGE2Path(file.filename);
	var a_url     = assets.getURL(localPath);
	var mime_app  = registry.getMimeType(localPath);
	if (mime_app) {
		// if it's a type registred by an app, override the mime type
		// in order to launch the right app
		mime_type = mime_app;
	} else {
		// otherwise use the mime type from exiftool
		mime_type = assets.getMimeType(localPath);
	}
	if (typeof a_url !== "string") {
		sageutils.log("Loader", "Cannot load app for file:", file);
		return;
	}
	var external_url = url.resolve(this.hostOrigin, a_url);

	this.loadApplication({
		location: "file", path: localPath, url: a_url, external_url: external_url,
		type: mime_type, name: file.filename, compressed: false, data: file.data}, function(appInstance, handle) {
		callback(appInstance, handle);
	});
};

AppLoader.prototype.manageAndLoadUploadedFile = function(file, callback) {
	// sanitize filename by remove odd charaters
	var cleanFilename = sanitize(file.name, "_");
	// Clean up further the file names
	cleanFilename = cleanFilename.replace(/[$%^&()'`\\/]/g, '_');

	// Check if there is a matching application
	var app = registry.getDefaultApp(cleanFilename);
	if (app === undefined || app === "") {
		callback(null);
		return;
	}
	var mime_type = registry.getMimeType(cleanFilename);
	var dir = registry.getDirectory(cleanFilename);
	var _this = this;

	if (!sageutils.folderExists(path.join(this.publicDir, dir))) {
		fs.mkdirSync(path.join(this.publicDir, dir));
	}

	// Check if it is a web-capable image, otherwise convert it to PNG
	if (mime_type.startsWith("image/")) {
		if (mime_type != "image/jpeg" && mime_type != "image/png" && mime_type != "image/webp") {
			sageutils.log("Loader", "converting image", cleanFilename);
			// setting up a tmp filename
			var tmpPath = path.join(this.publicDir, "tmp", path.basename(cleanFilename)) + ".png";
			// converting anything to PNG
			imageMagick(file.path).noProfile().bitdepth(8).flatten().setFormat("PNG").write(tmpPath, function(err, buffer) {
				if (err) {
					sageutils.log("Loader", "error processing image file", tmpPath);
					return;
				}
				// done with the tmp file
				fs.unlinkSync(file.path);
				// call same funtion again with the new PNG file
				return _this.manageAndLoadUploadedFile({name: cleanFilename + ".png", path: tmpPath}, callback);
			});
			// done for now
			return;
		}
	}

	// Use the defautl folder plus type as destination:
	//    SAGE2_Media/pdf/ for instance
	var localPath = path.join(this.publicDir, dir, cleanFilename);

	// Filename exists, then add date
	if (sageutils.fileExists(localPath)) {
		// Add the date to filename
		var filen  = cleanFilename;
		var splits = filen.split('.');
		var extension   = splits.pop();
		var newfilename = splits.join('_') + "_" + Date.now() + '.' + extension;
		localPath  = path.join(this.publicDir, dir, newfilename);
	}

	mv(file.path, localPath, function(err1) {
		if (err1) {
			throw err1;
		}
		if (app === "custom_app" && mime_type === "application/zip") {
			// Compressed ZIP file, load directly
			_this.loadApplication({
				location: "file", path: localPath, url: "", external_url: "",
				type: mime_type, name: cleanFilename, compressed: true}, function(appInstance, handle) {
				callback(appInstance, handle);
			});
		} else {
			// try to process all the files with exiftool
			exiftool.file(localPath, function(err2, data) {
				if (err2) {
					sageutils.log("Loader", "internal error", err2);
				} else {
					assets.addFile(data.SourceFile, data, function() {
						// get a valid URL for it
						var aUrl = assets.getURL(data.SourceFile);
						// calculate a complete URL with hostname
						var external_url = url.resolve(_this.hostOrigin, aUrl);
						// and load the application
						_this.loadApplication({
							location: "file", path: localPath, url: aUrl, external_url: external_url,
							type: mime_type, name: cleanFilename, compressed: false}, function(appInstance, handle) {
							callback(appInstance, handle);
						});
					});
					assets.saveAssets();
				}
			});
		}
	});
};

AppLoader.prototype.loadApplication = function(appData, callback) {
	var app;
	if (appData.location === "file") {
		app = registry.getDefaultAppFromMime(appData.type);
		if (app === "image_viewer") {
			this.loadImageFromFile(appData.path, appData.type, appData.url, appData.external_url, appData.name,
				function(appInstance) {
					callback(appInstance, null);
				});
		} else if (app === "movie_player") {
			this.loadVideoFromFile(appData.path, appData.type, appData.url, appData.external_url, appData.name,
				function(appInstance, handle) {
					callback(appInstance, handle);
				});
		} else if (app === "pdf_viewer") {
			this.loadPdfFromFile(appData.path, appData.type, appData.url, appData.external_url, appData.name,
				function(appInstance) {
					callback(appInstance, null);
				});
		} else if (app.indexOf("apps") >= 0 && app.indexOf("quickNote") >= 0) {
			this.loadNoteFromFile(appData.path, appData.type, appData.url, appData.external_url, appData.name,
				function(appInstance) {
					callback(appInstance, null);
				});
		} else if (app.indexOf("apps") >= 0 && app.indexOf("doodle") >= 0) {
			this.loadDoodleFromFile(appData.path, appData.type, appData.url, appData.external_url, appData.name,
				function(appInstance) {
					callback(appInstance, null);
				});
		} else if (app === "custom_app") {
			if (appData.compressed === true) {
				var name = path.basename(appData.name, path.extname(appData.name));
				var dir  = registry.getDirectory(appData.type);
				var futurePath = this.publicDir + dir + "/" + name;
				var localPath  = getSAGE2Path(futurePath);
				var aUrl = getSAGE2URL(localPath);
				var external_url = this.hostOrigin + sageutils.encodeReservedURL(aUrl);

				this.loadZipAppFromFile(appData.path, appData.type, aUrl, external_url, name,
					function(appInstance) {
						callback(appInstance, null);
					});
			} else {
				this.loadAppFromFile(appData.path, appData.type, appData.url, appData.external_url, appData.name, appData.data,
					function(appInstance) {
						callback(appInstance, null);
					});
			}
		} else {
			this.loadAppFromFileFromRegistry(appData.path, appData.type, appData.url, appData.external_url, appData.name,
				function(appInstance) {
					callback(appInstance);
				});
		}
	} else if (appData.location === "url") {
		app = registry.getDefaultAppFromMime(appData.type);

		if (app === "image_viewer") {
			this.loadImageFromURL(appData.url, appData.type, appData.name, appData.strictSSL, function(appInstance) {
				callback(appInstance, null);
			});
		} else if (app === "movie_player") {
			if (appData.type === "video/youtube") {
				this.loadYoutubeFromURL(appData.url, function(appInstance, handle) {
					callback(appInstance, handle);
				});
			} else {
				// Fixed size since cant process exif on URL yet
				this.loadVideoFromURL(appData.url, appData.type, appData.url, appData.name, function(appInstance, handle) {
					callback(appInstance, handle);
				});
			}
		} else if (app === "pdf_viewer") {
			this.loadPdfFromURL(appData.url, appData.type, appData.name, appData.strictSSL, function(appInstance) {
				callback(appInstance, null);
			});
		} else if (app === "Webview") {
			// Special case to load URLs in the webview app
			var webpath = getSAGE2Path('/uploads/apps/Webview');
			// Set the URL
			appData.data = {url: appData.url};
			// Set the path of the app
			appData.url  = webpath;
			// Load the webview
			this.loadAppFromFile(webpath, appData.type, appData.url, appData.url, "", appData.data,
				function(appInstance) {
					callback(appInstance, null);
				});
		}
	} else if (appData.location === "remote") {
		if (appData.application.application === "movie_player") {
			if (appData.application.type === "video/youtube") {
				this.loadYoutubeFromURL(appData.application.url, callback);
			} else {
				this.loadVideoFromURL(appData.application.url, appData.application.type,
					appData.application.url, appData.application.title, callback);
			}
		} else {
			var anInstance = {
				id: appData.application.id,
				title: appData.application.title,
				application: appData.application.application,
				type: appData.application.type,
				url: appData.application.url,
				data: appData.application.data,
				resrc: appData.application.resrc,
				left: this.titleBarHeight,
				top: 1.5 * this.titleBarHeight,
				width: appData.application.native_width,
				height: appData.application.native_height,
				native_width: appData.application.native_width,
				native_height: appData.application.native_height,
				previous_left: null,
				previous_top: null,
				previous_width: null,
				previous_height: null,
				maximized: false,
				aspect: appData.application.aspect,
				animation: appData.application.animation,
				metadata: appData.application.metadata,
				sticky: appData.application.sticky,
				plugin: appData.application.plugin,
				file: appData.application.file,
				sage2URL: appData.application.sage2URL,
				date: new Date()
			};
			if (appData.application.application === "pdf_viewer") {
				anInstance.data.doc_url = anInstance.url;
			}

			this.scaleAppToFitDisplay(anInstance);
			callback(anInstance, null);
		}
	}
};

AppLoader.prototype.readInstructionsFile = function(json_str, file, mime_type, external_url) {
	var instructions = JSON.parse(json_str);
	var appName = instructions.main_script.substring(0, instructions.main_script.lastIndexOf('.'));
	var aspectRatio = instructions.width / instructions.height;

	var resizeMode = "proportional";
	if (instructions.resize !== undefined && instructions.resize !== null && instructions.resize !== "") {
		resizeMode = instructions.resize;
	}

	var exif  = assets.getExifData(file);
	var s2url = assets.getURL(file);

	// Override custom app if a UnityLoader
	if (appName == "UnityLoader") {
		// Set type as WebView
		appName = "Webview";
		mime_type = "application/custom";
		var webpath = getSAGE2Path('/uploads/apps/Webview');
		external_url = this.hostOrigin + '/uploads/apps/Webview';

		// Load from the SAGE2 web server itself
		instructions.load = {
			url: this.hostOrigin + assets.getURL(file) + "/index.html",
			// set webview arguments
			zoom: 1,
			mode: "mobile",
			favicon: ""
		};
		file = webpath;
		s2url = '/uploads/apps/Webview';
	}

	var result = {
		id: null,
		title: exif.metadata.title,
		application: appName,
		icon: exif ? exif.SAGE2thumbnail : null,
		type: mime_type,
		url: external_url,
		data: instructions.load,
		resrc: instructions.dependencies,
		left: this.titleBarHeight,
		top: 1.5 * this.titleBarHeight,
		width: instructions.width,
		height: instructions.height,
		native_width: instructions.width,
		native_height: instructions.height,
		previous_left: null,
		previous_top: null,
		previous_width: null,
		previous_height: null,
		maximized: false,
		aspect: aspectRatio,
		animation: instructions.animation,
		metadata: exif.metadata,
		resizeMode: resizeMode,
		sticky: instructions.sticky,
		plugin: instructions.plugin,
		file: file,
		sage2URL: s2url,
		date: new Date()
	};
	return result;
};

/**
 * Determines if it exists in directory
 *
 * @method     existsInDir
 * @param      {<type>}           startDir  The start dir
 * @param      {<type>}           target    The target
 * @return     {String|String[]}  True if exists in dir, False otherwise.
 */
AppLoader.prototype.existsInDir = function(startDir, target) {
	if (!fs.existsSync(startDir)) {
		return;
	}
	var files = fs.readdirSync(startDir);
	for (var i = 0; i < files.length; i++) {
		var filename = path.join(startDir, files[i]);
		var stat = fs.lstatSync(filename);
		if (stat.isDirectory()) {
			var result = this.existsInDir(filename, target);
			if (result !== undefined) {
				return result;
			}
		}
		if (filename.indexOf(target) >= 0) {
			return filename;
		}
	}
	return;
};

module.exports = AppLoader;