apps.js 15 KB

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