apps.js 22 KB

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