apps.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, read, existsSync } from 'fs';
  2. import { parse } from 'yaml';
  3. import multer from 'multer';
  4. import AdmZip from 'adm-zip';
  5. import { json } from 'sequelize';
  6. const upload = multer({storage: multer.diskStorage({
  7. destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
  8. filename: function (req, file, cb) { cb(null, file.originalname) },
  9. })});
  10. let alert = '';
  11. let templates_global = '';
  12. let json_templates = '';
  13. export const Apps = async (req, res) => {
  14. let page = Number(req.params.page) || 1;
  15. let template_param = req.params.template || 'default';
  16. json_templates = '';
  17. let json_files = readdirSync('templates/json/');
  18. for (let i = 0; i < json_files.length; i++) {
  19. if (json_files[i] != 'default.json') {
  20. let filename = json_files[i].split('.')[0];
  21. let link = `<li><a class="dropdown-item" href="/apps/1/${filename}">${filename}</a></li>`
  22. json_templates += link;
  23. }
  24. }
  25. let apps_list = '';
  26. let app_count = '';
  27. let list_start = (page - 1) * 28;
  28. let list_end = (page * 28);
  29. let last_page = '';
  30. let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
  31. <li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
  32. <li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
  33. <li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
  34. <li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
  35. let prev = '/apps/' + (page - 1) + '/' + template_param;
  36. let next = '/apps/' + (page + 1) + '/' + template_param;
  37. if (page == 1) { prev = '/apps/' + (page) + '/' + template_param; }
  38. if (page == last_page) { next = '/apps/' + (page) + '/' + template_param;}
  39. if (template_param == 'compose') {
  40. let compose_files = readdirSync('templates/compose/');
  41. app_count = compose_files.length;
  42. last_page = Math.ceil(compose_files.length/28);
  43. compose_files.forEach(file => {
  44. let compose = readFileSync(`templates/compose/${file}/compose.yaml`, 'utf8');
  45. let compose_data = parse(compose);
  46. let service_name = Object.keys(compose_data.services)
  47. let container = compose_data.services[service_name].container_name;
  48. let image = compose_data.services[service_name].image;
  49. let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
  50. appCard = appCard.replace(/AppName/g, service_name);
  51. appCard = appCard.replace(/AppShortName/g, service_name);
  52. appCard = appCard.replace(/AppDesc/g, 'Compose File');
  53. appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
  54. appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
  55. appCard = appCard.replace(/AppType/g, 'compose');
  56. apps_list += appCard;
  57. });
  58. } else {
  59. let template_file = readFileSync(`./templates/json/${template_param}.json`);
  60. let templates = JSON.parse(template_file).templates;
  61. templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
  62. app_count = templates.length;
  63. templates_global = templates;
  64. apps_list = '';
  65. for (let i = list_start; i < list_end && i < templates.length; i++) {
  66. let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
  67. let name = templates[i].name || templates[i].title.toLowerCase();
  68. let title = templates[i].title || templates[i].name;
  69. let desc = templates[i].description.slice(0, 60) + "...";
  70. let description = templates[i].description.replaceAll(". ", ".\n") || "no description available";
  71. let note = templates[i].note ? templates[i].note.replaceAll(". ", ".\n") : "no notes available";
  72. let image = templates[i].image;
  73. let logo = templates[i].logo;
  74. let categories = '';
  75. // set data.catagories to 'other' if data.catagories is empty or undefined
  76. if (templates[i].categories == null || templates[i].categories == undefined || templates[i].categories == '') {
  77. templates[i].categories = ['Other'];
  78. }
  79. // loop through the categories and add the badge to the card
  80. for (let j = 0; j < templates[i].categories.length; j++) {
  81. categories += CatagoryColor(templates[i].categories[j]);
  82. }
  83. appCard = appCard.replace(/AppName/g, name);
  84. appCard = appCard.replace(/AppTitle/g, title);
  85. appCard = appCard.replace(/AppShortName/g, name);
  86. appCard = appCard.replace(/AppDesc/g, desc);
  87. appCard = appCard.replace(/AppLogo/g, logo);
  88. appCard = appCard.replace(/AppCategories/g, categories);
  89. appCard = appCard.replace(/AppType/g, 'json');
  90. apps_list += appCard;
  91. }
  92. }
  93. res.render("apps", {
  94. name: req.session.user,
  95. role: req.session.role,
  96. avatar: req.session.user.charAt(0).toUpperCase(),
  97. list_start: list_start + 1,
  98. list_end: list_end,
  99. app_count: app_count,
  100. prev: prev,
  101. next: next,
  102. apps_list: apps_list,
  103. alert: alert,
  104. template_list: '',
  105. json_templates: json_templates,
  106. pages: pages,
  107. });
  108. alert = '';
  109. }
  110. export const appSearch = async (req, res) => {
  111. let page = Number(req.params.page) || 1;
  112. let template_param = req.params.template || 'default';
  113. let template_file = readFileSync(`./templates/json/${template_param}.json`);
  114. let templates = JSON.parse(template_file).templates;
  115. templates = templates.sort((a, b) => {
  116. if (a.name < b.name) { return -1; }
  117. });
  118. let list_start = (page-1)*28;
  119. let list_end = (page*28);
  120. let last_page = Math.ceil(templates.length/28);
  121. let prev = '/apps/' + (page-1);
  122. let next = '/apps/' + (page+1);
  123. if (page == 1) { prev = '/apps/' + (page); }
  124. if (page == last_page) { next = '/apps/' + (page); }
  125. let search = req.body.search;
  126. let apps_list = '';
  127. let results = [];
  128. let [cat_1, cat_2, cat_3] = ['','',''];
  129. function searchTemplates(terms) {
  130. terms = terms.toLowerCase();
  131. for (let i = 0; i < templates.length; i++) {
  132. if (templates[i].categories) {
  133. if (templates[i].categories[0]) {
  134. cat_1 = (templates[i].categories[0]).toLowerCase();
  135. }
  136. if (templates[i].categories[1]) {
  137. cat_2 = (templates[i].categories[1]).toLowerCase();
  138. }
  139. if (templates[i].categories[2]) {
  140. cat_3 = (templates[i].categories[2]).toLowerCase();
  141. }
  142. }
  143. if ((templates[i].description.includes(terms)) || (templates[i].name.includes(terms)) || (templates[i].title.includes(terms)) || (cat_1.includes(terms)) || (cat_2.includes(terms)) || (cat_3.includes(terms))){
  144. results.push(templates[i]);
  145. }
  146. }
  147. }
  148. searchTemplates(search);
  149. for (let i = 0; i < results.length; i++) {
  150. let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
  151. let name = results[i].name || results[i].title.toLowerCase();
  152. let desc = results[i].description.slice(0, 60) + "...";
  153. let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
  154. let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
  155. let image = results[i].image;
  156. let logo = results[i].logo;let categories = '';
  157. // set data.catagories to 'other' if data.catagories is empty or undefined
  158. if (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
  159. results[i].categories = ['Other'];
  160. }
  161. // loop through the categories and add the badge to the card
  162. for (let j = 0; j < results[i].categories.length; j++) {
  163. categories += CatagoryColor(results[i].categories[j]);
  164. }
  165. appCard = appCard.replace(/AppName/g, name);
  166. appCard = appCard.replace(/AppShortName/g, name);
  167. appCard = appCard.replace(/AppDesc/g, desc);
  168. appCard = appCard.replace(/AppLogo/g, logo);
  169. appCard = appCard.replace(/AppCategories/g, categories);
  170. apps_list += appCard;
  171. }
  172. res.render("apps", {
  173. name: req.session.user,
  174. role: req.session.role,
  175. avatar: req.session.avatar,
  176. list_start: list_start + 1,
  177. list_end: list_end,
  178. app_count: results.length,
  179. prev: prev,
  180. next: next,
  181. apps_list: apps_list,
  182. alert: alert,
  183. template_list: '',
  184. });
  185. }
  186. function CatagoryColor(category) {
  187. switch (category) {
  188. case 'Other':
  189. return '<span class="badge bg-blue-lt">Other</span> ';
  190. case 'Productivity':
  191. return '<span class="badge bg-blue-lt">Productivity</span> ';
  192. case 'Tools':
  193. return '<span class="badge bg-blue-lt">Tools</span> ';
  194. case 'Dashboard':
  195. return '<span class="badge bg-blue-lt">Dashboard</span> ';
  196. case 'Communication':
  197. return '<span class="badge bg-azure-lt">Communication</span> ';
  198. case 'Media':
  199. return '<span class="badge bg-azure-lt">Media</span> ';
  200. case 'CMS':
  201. return '<span class="badge bg-azure-lt">CMS</span> ';
  202. case 'Monitoring':
  203. return '<span class="badge bg-indigo-lt">Monitoring</span> ';
  204. case 'LDAP':
  205. return '<span class="badge bg-purple-lt">LDAP</span> ';
  206. case 'Arr':
  207. return '<span class="badge bg-purple-lt">Arr</span> ';
  208. case 'Database':
  209. return '<span class="badge bg-red-lt">Database</span> ';
  210. case 'Paid':
  211. return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
  212. case 'Gaming':
  213. return '<span class="badge bg-pink-lt">Gaming</span> ';
  214. case 'Finance':
  215. return '<span class="badge bg-orange-lt">Finance</span> ';
  216. case 'Networking':
  217. return '<span class="badge bg-yellow-lt">Networking</span> ';
  218. case 'Authentication':
  219. return '<span class="badge bg-lime-lt">Authentication</span> ';
  220. case 'Development':
  221. return '<span class="badge bg-green-lt">Development</span> ';
  222. case 'Media Server':
  223. return '<span class="badge bg-teal-lt">Media Server</span> ';
  224. case 'Downloaders':
  225. return '<span class="badge bg-cyan-lt">Downloaders</span> ';
  226. default:
  227. return ''; // default to other if the category is not recognized
  228. }
  229. }
  230. export const InstallModal = async (req, res) => {
  231. let input = req.header('hx-trigger-name');
  232. let type = req.header('hx-trigger');
  233. if (type == 'compose') {
  234. let compose = readFileSync(`templates/compose/${input}/compose.yaml`, 'utf8');
  235. let modal = readFileSync('./views/modals/compose.html', 'utf8');
  236. modal = modal.replace(/AppName/g, input);
  237. modal = modal.replace(/COMPOSE_CONTENT/g, compose);
  238. res.send(modal);
  239. return;
  240. } else {
  241. let result = templates_global.find(t => t.name == input);
  242. let name = result.name || result.title.toLowerCase();
  243. let short_name = name.slice(0, 25) + "...";
  244. let desc = result.description.replaceAll(". ", ".\n") || "no description available";
  245. let short_desc = desc.slice(0, 60) + "...";
  246. let modal_name = name.replaceAll(" ", "-");
  247. let form_id = name.replaceAll("-", "_");
  248. let note = result.note ? result.note.replaceAll(". ", ".\n") : "no notes available";
  249. let command = result.command ? result.command : "";
  250. let command_check = command ? "checked" : "";
  251. let privileged = result.privileged || "";
  252. let privileged_check = privileged ? "checked" : "";
  253. let repository = result.repository || "";
  254. let image = result.image || "";
  255. let net_host, net_bridge, net_docker = '';
  256. let net_name = 'AppBridge';
  257. let restart_policy = result.restart_policy || 'unless-stopped';
  258. switch (result.network) {
  259. case 'host':
  260. net_host = 'checked';
  261. break;
  262. case 'bridge':
  263. net_bridge = 'checked';
  264. net_name = result.network;
  265. break;
  266. default:
  267. net_docker = 'checked';
  268. }
  269. if (repository != "") {
  270. image = (`${repository.url}/raw/master/${repository.stackfile}`);
  271. }
  272. let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
  273. for (let i = 0; i < 12; i++) {
  274. // Get port details
  275. try {
  276. let ports = result.ports[i];
  277. let port_check = ports ? "checked" : "";
  278. let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
  279. let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
  280. let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
  281. // remove /tcp or /udp from port_external if it exists
  282. if (port_external.includes("/")) {
  283. port_external = port_external.split("/")[0];
  284. }
  285. ports_data.push({
  286. check: port_check,
  287. external: port_external,
  288. internal: port_internal,
  289. protocol: port_protocol
  290. });
  291. } catch {
  292. ports_data.push({
  293. check: "",
  294. external: "",
  295. internal: "",
  296. protocol: ""
  297. });
  298. }
  299. // Get volume details
  300. try {
  301. let volumes = result.volumes[i];
  302. let volume_check = volumes ? "checked" : "";
  303. let volume_bind = volumes.bind ? volumes.bind : "";
  304. let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
  305. let volume_readwrite = "rw";
  306. if (volumes.readonly == true) {
  307. volume_readwrite = "ro";
  308. }
  309. volumes_data.push({
  310. check: volume_check,
  311. bind: volume_bind,
  312. container: volume_container,
  313. readwrite: volume_readwrite
  314. });
  315. } catch {
  316. volumes_data.push({
  317. check: "",
  318. bind: "",
  319. container: "",
  320. readwrite: ""
  321. });
  322. }
  323. // Get environment details
  324. try {
  325. let env = result.env[i];
  326. let env_check = "";
  327. let env_default = env.default ? env.default : "";
  328. if (env.set) { env_default = env.set;}
  329. let env_description = env.description ? env.description : "";
  330. let env_label = env.label ? env.label : "";
  331. let env_name = env.name ? env.name : "";
  332. env_data.push({
  333. check: env_check,
  334. default: env_default,
  335. description: env_description,
  336. label: env_label,
  337. name: env_name
  338. });
  339. } catch {
  340. env_data.push({
  341. check: "",
  342. default: "",
  343. description: "",
  344. label: "",
  345. name: ""
  346. });
  347. }
  348. // Get label details
  349. try {
  350. let label = result.labels[i];
  351. let label_check = "";
  352. let label_name = label.name ? label.name : "";
  353. let label_value = label.value ? label.value : "";
  354. label_data.push({
  355. check: label_check,
  356. name: label_name,
  357. value: label_value
  358. });
  359. } catch {
  360. label_data.push({
  361. check: "",
  362. name: "",
  363. value: ""
  364. });
  365. }
  366. }
  367. let modal = readFileSync('./views/modals/json.html', 'utf8');
  368. modal = modal.replace(/AppName/g, name);
  369. modal = modal.replace(/AppNote/g, note);
  370. modal = modal.replace(/AppImage/g, image);
  371. modal = modal.replace(/RestartPolicy/g, restart_policy);
  372. modal = modal.replace(/NetHost/g, net_host);
  373. modal = modal.replace(/NetBridge/g, net_bridge);
  374. modal = modal.replace(/NetDocker/g, net_docker);
  375. modal = modal.replace(/NetName/g, net_name);
  376. modal = modal.replace(/ModalName/g, modal_name);
  377. modal = modal.replace(/FormId/g, form_id);
  378. modal = modal.replace(/CommandCheck/g, command_check);
  379. modal = modal.replace(/CommandValue/g, command);
  380. modal = modal.replace(/PrivilegedCheck/g, privileged_check);
  381. for (let i = 0; i < 12; i++) {
  382. modal = modal.replaceAll(`Port${i}Check`, ports_data[i].check);
  383. modal = modal.replaceAll(`Port${i}External`, ports_data[i].external);
  384. modal = modal.replaceAll(`Port${i}Internal`, ports_data[i].internal);
  385. modal = modal.replaceAll(`Port${i}Protocol`, ports_data[i].protocol);
  386. modal = modal.replaceAll(`Volume${i}Check`, volumes_data[i].check);
  387. modal = modal.replaceAll(`Volume${i}Bind`, volumes_data[i].bind);
  388. modal = modal.replaceAll(`Volume${i}Container`, volumes_data[i].container);
  389. modal = modal.replaceAll(`Volume${i}RW`, volumes_data[i].readwrite);
  390. modal = modal.replaceAll(`Env${i}Check`, env_data[i].check);
  391. modal = modal.replaceAll(`Env${i}Default`, env_data[i].default);
  392. modal = modal.replaceAll(`Env${i}Description`, env_data[i].description);
  393. modal = modal.replaceAll(`Env${i}Label`, env_data[i].label);
  394. modal = modal.replaceAll(`Env${i}Name`, env_data[i].name);
  395. modal = modal.replaceAll(`Label${i}Check`, label_data[i].check);
  396. modal = modal.replaceAll(`Label${i}Name`, label_data[i].name);
  397. modal = modal.replaceAll(`Label${i}Value`, label_data[i].value);
  398. }
  399. res.send(modal);
  400. }
  401. }
  402. export const LearnMore = async (req, res) => {
  403. let name = req.header('hx-trigger-name');
  404. let id = req.header('hx-trigger');
  405. let result = templates_global.find(t => t.name == name);
  406. let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
  407. modal = modal.replace(/AppName/g, result.title);
  408. modal = modal.replace(/AppDesc/g, result.description);
  409. res.send(modal);
  410. }
  411. export const ImportModal = async (req, res) => {
  412. let modal = readFileSync('./views/modals/import.html', 'utf8');
  413. res.send(modal);
  414. }
  415. export const Upload = (req, res) => {
  416. upload.array('files', 10)(req, res, () => {
  417. alert = `<div class="alert alert-success alert-dismissible mb-0 py-2" role="alert">
  418. <div class="d-flex">
  419. <div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
  420. <div>Template(s) Uploaded!</div>
  421. </div>
  422. <a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
  423. </div>`;
  424. let exists_alert = `<div class="alert alert-danger alert-dismissible mb-0 py-2" role="alert">
  425. <div class="d-flex">
  426. <div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
  427. <div>Template already exists</div>
  428. </div>
  429. <a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
  430. </div>`;
  431. let files = readdirSync('templates/tmp/');
  432. for (let i = 0; i < files.length; i++) {
  433. if (files[i].endsWith('.zip')) {
  434. let zip = new AdmZip(`templates/tmp/${files[i]}`);
  435. zip.extractAllTo('templates/compose', true);
  436. unlinkSync(`templates/tmp/${files[i]}`);
  437. } else if (files[i].endsWith('.json')) {
  438. if (existsSync(`templates/json/${files[i]}`)) {
  439. unlinkSync(`templates/tmp/${files[i]}`);
  440. alert = exists_alert;
  441. res.redirect('/apps');
  442. return;
  443. }
  444. renameSync(`templates/tmp/${files[i]}`, `templates/json/${files[i]}`);
  445. } else if (files[i].endsWith('.yml')) {
  446. let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
  447. let compose_data = parse(compose);
  448. let service_name = Object.keys(compose_data.services);
  449. if (existsSync(`templates/compose/${service_name}`)) {
  450. unlinkSync(`templates/tmp/${files[i]}`);
  451. alert = exists_alert;
  452. res.redirect('/apps');
  453. return;
  454. }
  455. mkdirSync(`templates/compose/${service_name}`);
  456. renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
  457. } else if (files[i].endsWith('.yaml')) {
  458. let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
  459. let compose_data = parse(compose);
  460. let service_name = Object.keys(compose_data.services);
  461. if (existsSync(`templates/compose/${service_name}`)) {
  462. unlinkSync(`templates/tmp/${files[i]}`);
  463. alert = exists_alert;
  464. res.redirect('/apps');
  465. return;
  466. }
  467. mkdirSync(`templates/compose/${service_name}`);
  468. renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
  469. } else {
  470. // unsupported file type
  471. unlinkSync(`templates/tmp/${files[i]}`);
  472. }
  473. }
  474. res.redirect('/apps');
  475. });
  476. };