apps.js 17 KB

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