582 lines
22 KiB
JavaScript
582 lines
22 KiB
JavaScript
const { app, BrowserWindow, ipcMain, shell, systemPreferences } = require('electron');
|
|
|
|
|
|
const axios = require('axios');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const process = require('process');
|
|
|
|
//const http = require('http');
|
|
//const request = require('request');
|
|
//const url = require('url');
|
|
// const usb = require('usb') // Compiled with an old version of Node.js
|
|
|
|
console.log(`OS: ${os.type()} ${process.getSystemVersion()}`);
|
|
// console.log(process.getSystemVersion());
|
|
|
|
let home_directory = require('os').homedir();
|
|
console.log('Home: '+home_directory);
|
|
|
|
let tmp_directory = require('os').tmpdir();
|
|
console.log('Temporary: '+tmp_directory);
|
|
|
|
// Set the config path for macOS or Linux
|
|
let config_directory = 'OSIT/native_app';
|
|
let config_filename = 'ae_native_app_config.json';
|
|
let config_path = '';
|
|
|
|
let local_file_cache_path = null;
|
|
let host_file_temp_path = null;
|
|
|
|
let endpoints_in_progress = [];
|
|
|
|
/* Look for and load a JSON formatted config file. */
|
|
if (os.platform == 'darwin') {
|
|
let config_path_default = path.join(home_directory, config_directory, config_filename);
|
|
let config_path_macos = path.join(home_directory, 'Library/Application Support/OSIT', config_filename);
|
|
// let config_path_opt2 = path.join(home_directory, 'OSIT', config_filename);
|
|
|
|
if (fs.existsSync(config_path_default)) {
|
|
console.log('Default config file path exists: '+config_path_default);
|
|
config_path = config_path_default;
|
|
} else if (fs.existsSync(config_path_macos)) {
|
|
console.log('macOS config file path exists: '+config_path_macos);
|
|
config_path = config_path_macos;
|
|
} else {
|
|
console.log(`No config file found: ${config_path_default} or ${config_path_macos}`);
|
|
config_path = '';
|
|
// fs.mkdirSync(config_file_directory_path, true);
|
|
// console.log('Config directory path created: '+config_file_directory_path);
|
|
}
|
|
|
|
console.log(`Using config found on macOS: ${config_path}`);
|
|
} else if (os.platform == 'linux') {
|
|
let config_path_default = path.join(home_directory, config_directory, config_filename);
|
|
let config_path_linux_os = path.join(home_directory, '.config/OSIT', config_filename);
|
|
let config_path_temp = path.join(home_directory, 'tmp/OSIT', config_filename);
|
|
|
|
if (fs.existsSync(config_path_default)) {
|
|
console.log('Default config file path exists: '+config_path_default);
|
|
config_path = config_path_default;
|
|
} else if (fs.existsSync(config_path_linux_os)) {
|
|
console.log('Linux config file path exists: '+config_path_linux_os);
|
|
config_path = config_path_linux_os;
|
|
} else if (fs.existsSync(config_path_temp)) {
|
|
console.log('Temp config file path exists: '+config_path_temp);
|
|
config_path = config_path_temp;
|
|
} else {
|
|
console.log(`No config file found: ${config_path_default} or ${config_path_linux_os} or ${config_path_temp}`);
|
|
config_path = '';
|
|
}
|
|
|
|
console.log(`Using config found on Linux: ${config_path}`);
|
|
}
|
|
|
|
let config = JSON.parse(fs.readFileSync(config_path));
|
|
console.log('Config file read.', config);
|
|
/*
|
|
Minimal configuration contains:
|
|
* conf_file_check_path = '~/OSIT/sync/admin_share/internal/ae_osit_app.default.conf'
|
|
* conf_file_check_path_backup = 'ae_osit_app.conf'
|
|
|
|
* api_pref_use = 'local' or 'remote' or 'backup'
|
|
* api_base_url_local = https://local-api.oneskyit.com
|
|
* api_base_url_remote = https://api.oneskyit.com
|
|
* api_base_url_backup = https://bak-api.oneskyit.com
|
|
|
|
* app_pref_use = 'local' or 'remote' or 'backup'
|
|
* app_base_url_local = https://local-demo.oneskyit.com
|
|
* app_base_url_remote = https://demo.oneskyit.com
|
|
* app_base_url_backup = https://bak-demo.oneskyit.com
|
|
|
|
* device_id = 'abcd1234'
|
|
*/
|
|
|
|
|
|
/*
|
|
Ask for permissions from macOS to use the microphone, screen, camera. The OS may delay actually asking for permission until the permission is actually attempted to be used. It may be worth doing a test attempt early on if access has not already been granted. -STI 2023-06-03
|
|
*/
|
|
if (os.type == 'Darwin') {
|
|
if (systemPreferences.getMediaAccessStatus('microphone') != 'granted') {
|
|
systemPreferences.askForMediaAccess('microphone');
|
|
} else {
|
|
console.log('Microphone access:', systemPreferences.getMediaAccessStatus('microphone'));
|
|
}
|
|
|
|
if (systemPreferences.getMediaAccessStatus('screen') != 'granted') {
|
|
systemPreferences.askForMediaAccess('screen');
|
|
} else {
|
|
console.log('Screen access:', systemPreferences.getMediaAccessStatus('screen'));
|
|
}
|
|
|
|
if (systemPreferences.getMediaAccessStatus('camera') != 'granted') {
|
|
systemPreferences.askForMediaAccess('camera');
|
|
} else {
|
|
console.log('Camera access:', systemPreferences.getMediaAccessStatus('camera'));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function get_url_cfg() {
|
|
let base_url = `${config.api_protocol}://${config.api_server}:${config.api_port}/${config.api_path}`;
|
|
|
|
let axios_api = axios.create({
|
|
baseURL: base_url,
|
|
timeout: 60000, // in milliseconds; 60000 = 60 seconds
|
|
/* other custom settings */
|
|
});
|
|
// axios_api.defaults.headers = config['headers'];
|
|
|
|
// axios.defaults.baseURL = `${config.api_protocol}://${config.api_server}:${config.api_port}/${config.api_path}`;
|
|
axios_api.defaults.headers.common['Access-Control-Allow-Origin'] = config.access_control_allow_origin; // '*'; // app_config.access_control_allow_origin;
|
|
axios_api.defaults.headers.common['content-type'] = 'application/json';
|
|
axios_api.defaults.headers.common['x-aether-api-key'] = config.api_secret_key;
|
|
axios_api.defaults.headers.common['x-account-id'] = config.account_id;
|
|
|
|
let event_device_id = 'dbgMWS3KEHE';
|
|
let endpoint = `/event/device/${event_device_id}`;
|
|
|
|
let params = {'event_device_code': 'asdf'};
|
|
|
|
let response_data_promise = await axios_api.get(
|
|
endpoint,
|
|
{
|
|
params: params,
|
|
onDownloadProgress: (progressEvent) => {
|
|
let percent_completed = Math.round(
|
|
(progressEvent.loaded * 100) / progressEvent.total
|
|
);
|
|
console.log('GET Data Timestamp:', progressEvent.timeStamp, 'Total:', progressEvent.total, 'Loaded:', progressEvent.loaded, 'Percent Completed', percent_completed);
|
|
|
|
// temp_get_object_percent_completed = percent_completed;
|
|
}
|
|
}
|
|
)
|
|
.then(function (response) {
|
|
console.log(`Response: ${response}`);
|
|
|
|
let return_data = response.data['data'];
|
|
if (Array.isArray(return_data)) {
|
|
console.log(`Data result is an array/list. Array length: ${return_data.length}`);
|
|
} else {
|
|
console.log(`Data result is a dictionary/object, not an array/list.`);
|
|
}
|
|
return return_data;
|
|
})
|
|
.catch(function (error) {
|
|
console.log(`Base URL: ${base_url} | Endpoint: ${endpoint}`);
|
|
|
|
console.log('Error Message:', error.message); // Is this needed here or below in the in the else portion???
|
|
if (error.response) {
|
|
console.log(`Response Status: ${error.response.status}; Status Text: ${error.response.statusText}`);
|
|
} else {
|
|
console.log('Error:', error);
|
|
}
|
|
|
|
if (error.response && error.response.status === 404) {
|
|
return null; // Returning null since there were no results
|
|
}
|
|
return false; // Returning false since something may have gone wrong. Also more in line with what the API returns.
|
|
});
|
|
|
|
return response_data_promise;
|
|
}
|
|
|
|
let new_config = get_url_cfg();
|
|
console.log(new_config);
|
|
|
|
|
|
|
|
|
|
function createWindow () {
|
|
// Create the browser window.
|
|
win = new BrowserWindow({
|
|
width: 1500, // 1500 1280
|
|
height: 1024, // 1024
|
|
backgroundColor: '#aaa',
|
|
icon: './app/img/favicon.ico',
|
|
webPreferences: {
|
|
contextIsolation: false,
|
|
nodeIntegration: true,
|
|
nodeIntegrationInWorker: true,
|
|
enableRemoteModule: true
|
|
}
|
|
})
|
|
|
|
// win.setMinimumSize(1024, 768);
|
|
// win.setMinimumSize(1280, 768);
|
|
win.setMinimumSize(1400, 768);
|
|
|
|
//win.setFullScreenable(false)
|
|
win.FullScreenable = false;
|
|
|
|
// win.webContents.session.clearStorageData(['filesystem']); // Does this do anything???
|
|
|
|
// native_app_which_html = 'default', 'path', 'url'
|
|
// 'default' (internal) is within the bundled native app
|
|
// 'path' (external to app) is a file path on the host, probably under the home directory
|
|
// 'url' is over HTTPS, maybe onsite or offsite
|
|
|
|
// Load the index.html of the app
|
|
if (config.native_app_which_html == '' || config.native_app_which_html == 'default') {
|
|
win.loadFile('app/index.html');
|
|
} else if (config.native_app_which_html == 'path') {
|
|
let index_path = 'index.html';
|
|
|
|
if (config.native_app_index_path) {
|
|
index_path = config.native_app_index_path.replace('[home]', home_directory);
|
|
} else {
|
|
index_path = path.join(home_directory, 'OSIT/native_app/app/index.html');
|
|
}
|
|
win.loadFile(index_path);
|
|
} else if (config.native_app_which_html == 'url') {
|
|
let index_url = 'http://localhost/index.html';
|
|
|
|
if (config.native_app_index_url) {
|
|
index_url = config.native_app_index_url;
|
|
} else {
|
|
index_url = 'https://app.oneskyit.com/native/index.html';
|
|
}
|
|
win.loadURL(index_url);
|
|
} else {
|
|
win.loadFile('app/index.html');
|
|
}
|
|
|
|
// Open the DevTools.
|
|
if (config.developer_tools) {
|
|
win.webContents.openDevTools(); // Comment out for production
|
|
}
|
|
|
|
// Emitted when the window is closed.
|
|
win.on('closed', () => {
|
|
// Dereference the window object, usually you would store windows
|
|
// in an array if your app supports multi windows, this is the time
|
|
// when you should delete the corresponding element.
|
|
win = null;
|
|
})
|
|
|
|
win.on('minimize', () => {
|
|
//win.restore();
|
|
})
|
|
}
|
|
|
|
|
|
// This method will be called when Electron has finished
|
|
// initialization and is ready to create browser windows.
|
|
// Some APIs can only be used after this event occurs.
|
|
app.on('ready', createWindow);
|
|
|
|
|
|
// Quit when all windows are closed.
|
|
app.on('window-all-closed', () => {
|
|
// On macOS it is common for applications and their menu bar
|
|
// to stay active until the user quits explicitly with Cmd + Q
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
})
|
|
|
|
|
|
app.on('activate', () => {
|
|
// On macOS it's common to re-create a window in the app when the
|
|
// dock icon is clicked and there are no other windows open.location_files
|
|
if (win === null) {
|
|
createWindow();
|
|
}
|
|
})
|
|
|
|
|
|
// Import config data
|
|
// Updated 2022-04-16
|
|
ipcMain.handle('import_config', async (event, config_data) => {
|
|
console.log('*** Electron IPC Main: import_config() ***');
|
|
// console.log('ipcMain on download_file: api_base_url='+api_base_url+' | api_temporary_token='+api_temporary_token);
|
|
console.log('ipcMain on import_config:');
|
|
console.log(config_data);
|
|
|
|
config = config_data;
|
|
|
|
local_file_cache_path = config.local_file_cache_path;
|
|
host_file_temp_path = config.host_file_temp_path;
|
|
|
|
if (fs.existsSync(local_file_cache_path)) {
|
|
console.log('Host file cache path exists: '+local_file_cache_path);
|
|
} else {
|
|
fs.mkdirSync(local_file_cache_path, true);
|
|
console.log('Host file cache path created: '+local_file_cache_path);
|
|
}
|
|
|
|
if (fs.existsSync(host_file_temp_path)) {
|
|
console.log('Host file temp path exists: '+host_file_temp_path);
|
|
} else {
|
|
fs.mkdirSync(host_file_temp_path, true);
|
|
console.log('Host file temp path created: '+host_file_temp_path);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
|
|
// Download file to path
|
|
// full_save_path should be the full path that includes the filename
|
|
// Updated 2023-05-14
|
|
ipcMain.handle('download_file', async (event, api_base_url, api_endpoint, full_save_path, hash=null, verify_hash=false, overwrite_existing=false, offset_minutes=3) => {
|
|
console.log('*** Electron IPC Main: download_file() ***');
|
|
// console.log('ipcMain on download_file: api_base_url='+api_base_url+' | api_temporary_token='+api_temporary_token);
|
|
// console.log('ipcMain on download_file: api_base_url='+api_base_url);
|
|
console.log(`ipcMain download and save file: HTTP ${api_endpoint} -> FILE ${full_save_path}`);
|
|
if (!api_base_url) {
|
|
console.log('API Base URL is required. Returning false');
|
|
return false;
|
|
}
|
|
|
|
axios.defaults.baseURL = api_base_url;
|
|
axios.defaults.headers.common['Access-Control-Allow-Origin'] = config.access_control_allow_origin; // '*'; // app_config.access_control_allow_origin;
|
|
axios.defaults.headers.common['content-type'] = 'application/json';
|
|
axios.defaults.headers.common['x-aether-api-key'] = config.api_secret_key;
|
|
axios.defaults.headers.common['x-account-id'] = config.account_id;
|
|
|
|
const url = api_endpoint;
|
|
|
|
const tmp_full_save_path = full_save_path+'.tmp';
|
|
|
|
if (fs.existsSync(tmp_full_save_path)) {
|
|
console.log(`A temp download file was found! ${tmp_full_save_path}`);
|
|
|
|
let stats = null;
|
|
try {
|
|
stats = fs.statSync(tmp_full_save_path);
|
|
|
|
// console.log(`File Accessed Last: ${stats.atime}`); // File data last changed (actual contents)
|
|
console.log(`File Data Last Modified: ${stats.mtime}`); // File data last changed (actual contents)
|
|
console.log(`File Metadata Last Modified: ${stats.ctime}`); // File metadata last changed (filename, permissions, etc)
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
|
|
let current_datetime = new Date();
|
|
// In minutes. After 5ish minutes of no changes to the file content seems reasonable? Tested with 3 minutes for multiple meetings and no noticable problem.
|
|
let offset_datetime = new Date(current_datetime.getTime() - offset_minutes*60000);
|
|
|
|
// console.log(`Times: ${current_datetime} ${offset_datetime} | File ${stats.mtime}`);
|
|
if (stats.mtime < offset_datetime) {
|
|
console.log(`Marking as expired temp file based on modified datetime. Expire after: ${offset_minutes} minutes`);
|
|
overwrite_existing = true;
|
|
} else {
|
|
console.log(`Temp download file has not expired yet. Expire after: ${offset_minutes} minutes`);
|
|
// return false;
|
|
return 'tmp';
|
|
}
|
|
}
|
|
if (fs.existsSync(full_save_path)) {
|
|
console.log(`A cached file was found! ${full_save_path}`);
|
|
if (verify_hash) {
|
|
const file_buffer = fs.readFileSync(full_save_path);
|
|
const file_hash_sha256 = crypto.createHash('sha256');
|
|
file_hash_sha256.update(file_buffer);
|
|
|
|
const file_hash_sha256_check = file_hash_sha256.digest('hex');
|
|
if (file_hash_sha256_check == hash) {
|
|
// console.log('File hash match', file_hash_sha256_check);
|
|
} else {
|
|
console.log('File hash does not match', file_hash_sha256_check);
|
|
if (overwrite_existing) {
|
|
console.log('Going to overwrite the existing file because the hash does not match.');
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// return false;
|
|
}
|
|
|
|
console.log('Endpoints in Progress:', endpoints_in_progress);
|
|
if (endpoints_in_progress.includes(api_endpoint)) {
|
|
console.log(`Endpoint already being downloaded: ${api_endpoint}`);
|
|
// return false;
|
|
return 'in_progress';
|
|
}
|
|
// console.log(`Done with checks. Time to download! Endpoint: ${api_endpoint}`);
|
|
endpoints_in_progress.push(api_endpoint);
|
|
|
|
let download_result = await axios({
|
|
method: 'get',
|
|
url: url,
|
|
responseType: 'stream' /* responseType must be stream */
|
|
}).then(function (response) {
|
|
console.log(`Creating write stream for downloading endpoint: ${api_endpoint}`);
|
|
const writer = fs.createWriteStream(tmp_full_save_path);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
response.data.pipe(writer);
|
|
let error = null;
|
|
writer.on('error', err => {
|
|
console.log('Writer error!');
|
|
error = err;
|
|
console.log(error);
|
|
writer.close();
|
|
reject(err);
|
|
});
|
|
writer.on('close', () => {
|
|
|
|
if (!error) {
|
|
// console.log(`Download complete! Writer closed.`);
|
|
resolve(true);
|
|
} else {
|
|
console.log('Writer closed unexpectedly!', error);
|
|
}
|
|
//no need to call the reject here, as it will have been called in the
|
|
//'error' stream;
|
|
});
|
|
});
|
|
})
|
|
.then(function (response) {
|
|
console.log(`Download complete. Temporary file moved/renamed: ${full_save_path}`);
|
|
fs.renameSync(tmp_full_save_path, full_save_path);
|
|
|
|
for( let i = 0; i < endpoints_in_progress.length; i++){
|
|
if ( endpoints_in_progress[i] === api_endpoint) {
|
|
endpoints_in_progress.splice(i, 1);
|
|
// NOTE: Decrement the index variable so it does not skip the next item in the array.
|
|
i--;
|
|
}
|
|
}
|
|
return true;
|
|
})
|
|
.catch(function (error) {
|
|
console.log(`Error downloading! Endpoint: ${api_endpoint}`);
|
|
|
|
for( let i = 0; i < endpoints_in_progress.length; i++){
|
|
if ( endpoints_in_progress[i] === api_endpoint) {
|
|
endpoints_in_progress.splice(i, 1);
|
|
// NOTE: Decrement the index variable so it does not skip the next item in the array.
|
|
i--;
|
|
}
|
|
}
|
|
|
|
if (error.response) {
|
|
console.log(`Response Status: ${error.response.status}; Status Text: ${error.response.statusText}`);
|
|
} else {
|
|
console.log('Error:', error);
|
|
}
|
|
|
|
if (error.response && error.response.status === 404) {
|
|
return null; // Returning null since there were no results
|
|
}
|
|
return false; // Returning false since something may have gone wrong. Also more in line with what the API returns.
|
|
});
|
|
// console.log(`Done with download function! Endpoint: ${api_endpoint}`);
|
|
|
|
return download_result;
|
|
});
|
|
|
|
|
|
ipcMain.handle('open_hash_file_to_temp', async (event, local_file_cache_path, hash, host_file_temp_path, filename, verify_hash=true) => {
|
|
console.log('*** Electron IPC Main: open_hash_file_to_temp() ***');
|
|
console.log('ipcMain on open_hash_file_to_temp');
|
|
console.log(`ipcMain open hash file from temp directory: ${local_file_cache_path} -> ${host_file_temp_path}/${filename}`);
|
|
|
|
// NOTE: This may be needed later? Uncomment if paths are relative to working directory.
|
|
// let cache_file_path = path.join(process.cwd(), local_file_cache_path);
|
|
let cache_file_path = local_file_cache_path;
|
|
console.log(cache_file_path);
|
|
|
|
let hash_filename = hash+'.file';
|
|
let full_cache_file_path = path.join(cache_file_path, hash_filename);
|
|
console.log(full_cache_file_path);
|
|
|
|
// NOTE: This may be needed later? Uncomment if paths are relative to working directory.
|
|
// open_temp_file_path = path.join(process.cwd(), host_file_temp_path, filename); // 'temp/'
|
|
open_temp_file_path = path.join(host_file_temp_path, filename); // 'temp/'
|
|
console.log(open_temp_file_path);
|
|
|
|
if (fs.existsSync(open_temp_file_path)) {
|
|
console.log('A file with the same name already exists in the local temp directory: '+open_temp_file_path);
|
|
// NOTE: Should the file be checked to see if it has changed from the hashed cache version???
|
|
// NOTE: What if they made changes to the file locally in temp? The changed file would be used since a new copy is not being made.
|
|
// NOTE: It might make sense for this to be a configurable option depending on the group. Some do not allow changes. This helps enforce that.
|
|
}
|
|
|
|
if (fs.existsSync(full_cache_file_path)) {
|
|
// console.log(`Hashed file exists in cache: ${full_cache_file_path}`);
|
|
console.log(`Copying file to temp: ${open_temp_file_path}`);
|
|
try {
|
|
fs.copyFileSync(full_cache_file_path, open_temp_file_path);
|
|
} catch (error) {
|
|
console.error(error);
|
|
return false;
|
|
}
|
|
|
|
if (verify_hash) {
|
|
const file_buffer = fs.readFileSync(full_cache_file_path);
|
|
const file_hash_sha256 = crypto.createHash('sha256');
|
|
file_hash_sha256.update(file_buffer);
|
|
|
|
const file_hash_sha256_check = file_hash_sha256.digest('hex');
|
|
if (file_hash_sha256_check == hash) {
|
|
// console.log('File hash match', file_hash_sha256_check);
|
|
} else {
|
|
console.log('File hash does not match', file_hash_sha256_check);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// console.log('Creating file link: '+open_temp_file_path);
|
|
// fs.linkSync(full_cache_file_path, open_temp_file_path);
|
|
} else {
|
|
console.log(`Hashed file not found in cache: ${full_cache_file_path}`);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await shell.openPath(open_temp_file_path);
|
|
} catch (error) {
|
|
console.error(error);
|
|
return false;
|
|
}
|
|
|
|
console.log('End: Electron IPC Main: open_hash_file_to_temp()');
|
|
return true;
|
|
});
|
|
|
|
|
|
ipcMain.handle('open_local_file', async (event, local_file_path, filename, use_cwd=true) => {
|
|
console.log('*** Electron IPC Main: open_local_file() ***');
|
|
console.log('ipcMain on open_local_file');
|
|
console.log(`ipcMain open local file from directory: ${local_file_path}/${filename}`);
|
|
|
|
let full_local_file_path = null;
|
|
|
|
if (use_cwd) {
|
|
full_local_file_path = path.join(process.cwd(), local_file_path, filename);
|
|
console.log(full_local_file_path);
|
|
} else {
|
|
full_local_file_path = path.join(local_file_path, filename);
|
|
console.log(full_local_file_path);
|
|
}
|
|
|
|
if (fs.existsSync(full_local_file_path)) {
|
|
console.log(`Local file exists: ${full_local_file_path}`);
|
|
} else {
|
|
console.log(`Local file not found: ${full_local_file_path}`);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await shell.openPath(full_local_file_path);
|
|
} catch (error) {
|
|
console.error(error);
|
|
return false;
|
|
}
|
|
|
|
console.log('End: Electron IPC Main: open_local_file()');
|
|
return true;
|
|
}); |