apps.js 17 KB

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