shares.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <!--
  2. Copyright (C) 2023 Nicola Murino
  3. This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
  4. https://keenthemes.com/products/templates-mega-bundle
  5. KeenThemes HTML/CSS/JS components are allowed for use only within the
  6. SFTPGo product and restricted to be used in a resealable HTML template
  7. that can compete with KeenThemes products anyhow.
  8. This WebUI is allowed for use only within the SFTPGo product and
  9. therefore cannot be used in derivative works/products without an
  10. explicit grant from the SFTPGo Team ([email protected]).
  11. -->
  12. {{template "base" .}}
  13. {{- define "extra_css"}}
  14. <link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
  15. {{- end}}
  16. {{define "page_body"}}
  17. <div class="card shadow-sm">
  18. <div class="card-header bg-light">
  19. <h3 data-i18n="share.view_manage" class="card-title section-title">View and manage shares</h3>
  20. </div>
  21. <div id="card_body" class="card-body">
  22. <div id="loader" class="align-items-center text-center my-10">
  23. <span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
  24. <span data-i18n="general.loading" class="text-gray-600">Loading...</span>
  25. </div>
  26. <div id="card_content" class="d-none">
  27. <div class="d-flex flex-stack mb-5">
  28. <div class="d-flex align-items-center position-relative my-1">
  29. <i class="ki-duotone ki-magnifier fs-1 position-absolute ms-6">
  30. <span class="path1"></span>
  31. <span class="path2"></span>
  32. </i>
  33. <input name="search" data-i18n="[placeholder]general.search" type="text" data-share-table-filter="search"
  34. class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
  35. </div>
  36. <div class="d-flex justify-content-end" data-share-table-toolbar="base">
  37. <a data-i18n="general.add" href="{{.ShareURL}}" class="btn btn-primary">
  38. <i class="ki-duotone ki-plus fs-2"></i>
  39. Add
  40. </a>
  41. </div>
  42. </div>
  43. <table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
  44. <thead>
  45. <tr class="text-start text-muted fw-bold fs-6 gs-0">
  46. <th data-i18n="general.name">Name</th>
  47. <th data-i18n="share.scope">Scope</th>
  48. <th data-i18n="general.info">Info</th>
  49. <th class="min-w-100px"></th>
  50. </tr>
  51. </thead>
  52. <tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
  53. </table>
  54. </div>
  55. </div>
  56. </div>
  57. {{- end}}
  58. {{- define "modals"}}
  59. <div class="modal fade" id="link_modal" tabindex="-1">
  60. <div class="modal-dialog" role="document">
  61. <div class="modal-content">
  62. <div class="modal-header border-0">
  63. <h3 data-i18n="share.access_links_title" class="modal-title">
  64. Share access links
  65. </h3>
  66. <div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-light-primary" data-bs-dismiss="modal" aria-label="Close">
  67. <i class="ki-solid ki-cross fs-2x text-gray-700"></i>
  68. </div>
  69. </div>
  70. <div class="modal-body fs-5">
  71. <div id="readShare">
  72. <div class="mb-5">
  73. <h4 data-i18n="share.link_single_title">Single zip file</h4>
  74. <p data-i18n="share.link_single_desc">You can download shared content as a single zip file</p>
  75. <div class="d-flex">
  76. <button id="readLinkCopy" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
  77. <i class="ki-duotone ki-fasten fs-2">
  78. <span class="path1"></span>
  79. <span class="path2"></span>
  80. </i>
  81. <span data-i18n="general.copy_link">Copy link</span>
  82. </button>
  83. <a id="readLink" href="#" target="_blank" type="button" class="btn btn-flex btn-primary">
  84. <i class="ki-duotone ki-folder-down fs-2">
  85. <span class="path1"></span>
  86. <span class="path2"></span>
  87. </i>
  88. <span data-i18n="fs.download">Download</span>
  89. </a>
  90. </div>
  91. </div>
  92. <hr>
  93. <div class="mb-5">
  94. <h4 data-i18n="share.link_dir_title">Single directory</h4>
  95. <p data-i18n="share.link_dir_desc">If the share consists of a single directory you can browse and download files</p>
  96. <button id="readBrowseLinkCopy" data-clipboard-target="#readBrowseLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
  97. <i class="ki-duotone ki-fasten fs-2">
  98. <span class="path1"></span>
  99. <span class="path2"></span>
  100. </i>
  101. <span data-i18n="general.copy_link">Copy link</span>
  102. </button>
  103. <a id="readBrowseLink" href="#" target="_blank" type="button" class="btn btn-flex btn-primary">
  104. <i class="ki-duotone ki-arrow-up-right fs-2">
  105. <span class="path1"></span>
  106. <span class="path2"></span>
  107. </i>
  108. <span data-i18n="share.go">Go to share</span>
  109. </a>
  110. </div>
  111. <hr>
  112. <div>
  113. <h4 data-i18n="share.link_uncompressed_title">Uncompressed file</h4>
  114. <p data-i18n="share.link_uncompressed_desc">If the share consists of a single file you can download it uncompressed</p>
  115. <button id="readUncompressedLinkCopy" data-clipboard-target="#readUncompressedLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
  116. <i class="ki-duotone ki-fasten fs-2">
  117. <span class="path1"></span>
  118. <span class="path2"></span>
  119. </i>
  120. <span data-i18n="general.copy_link">Copy link</span>
  121. </button>
  122. <a id="readUncompressedLink" href="#" target="_blank" type="button" class="btn btn-flex btn-primary">
  123. <i class="ki-duotone ki-folder-down fs-2">
  124. <span class="path1"></span>
  125. <span class="path2"></span>
  126. </i>
  127. <span data-i18n="fs.download">Download</span>
  128. </a>
  129. </div>
  130. </div>
  131. <div id="writeShare">
  132. <p data-i18n="share.upload_desc">You can upload one or more files to the shared directory</p>
  133. <button id="writePageLinkCopy" data-clipboard-target="#writePageLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
  134. <i class="ki-duotone ki-fasten fs-2">
  135. <span class="path1"></span>
  136. <span class="path2"></span>
  137. </i>
  138. <span data-i18n="general.copy_link">Copy link</span>
  139. </button>
  140. <a id="writePageLink" href="#" target="_blank" type="button" class="btn btn-flex btn-primary">
  141. <i class="ki-duotone ki-folder-up fs-2">
  142. <span class="path1"></span>
  143. <span class="path2"></span>
  144. </i>
  145. <span data-i18n="fs.upload.text">Upload</span>
  146. </a>
  147. </div>
  148. <div data-i18n="share.expired_desc" id="expiredShare">
  149. This share is no longer accessible because it has expired
  150. </div>
  151. </div>
  152. </div>
  153. </div>
  154. </div>
  155. {{end}}
  156. {{define "extra_js"}}
  157. <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
  158. <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
  159. function deleteAction(shareID) {
  160. ModalAlert.fire({
  161. text: $.t('general.delete_confirm_generic'),
  162. icon: "warning",
  163. confirmButtonText: $.t('general.delete_confirm_btn'),
  164. cancelButtonText: $.t('general.cancel'),
  165. customClass: {
  166. confirmButton: "btn btn-danger",
  167. cancelButton: 'btn btn-secondary'
  168. }
  169. }).then((result) => {
  170. if (result.isConfirmed){
  171. $('#loading_message').text("");
  172. KTApp.showPageLoading();
  173. let path = '{{.ShareURL}}' + "/" + encodeURIComponent(shareID);
  174. axios.delete(path, {
  175. timeout: 15000,
  176. headers: {
  177. 'X-CSRF-TOKEN': '{{.CSRFToken}}'
  178. },
  179. validateStatus: function (status) {
  180. return status == 200;
  181. }
  182. }).then(function(response){
  183. location.reload();
  184. }).catch(function(error){
  185. KTApp.hidePageLoading();
  186. let errorMessage;
  187. if (error && error.response) {
  188. switch (error.response.status) {
  189. case 403:
  190. errorMessage = "general.delete_error_403";
  191. break;
  192. case 404:
  193. errorMessage = "general.delete_error_404";
  194. break;
  195. }
  196. }
  197. if (!errorMessage){
  198. errorMessage = "general.delete_error_generic";
  199. }
  200. showToast(errorMessage);
  201. });
  202. }
  203. });
  204. }
  205. function editAction(shareID) {
  206. window.location.replace('{{.ShareURL}}' + "/" + encodeURIComponent(shareID));
  207. }
  208. function showShareLink(shareID, shareScope, isExpired) {
  209. if (isExpired == "1") {
  210. $('#expiredShare').show();
  211. $('#writeShare').hide();
  212. $('#readShare').hide();
  213. } else {
  214. let shareURL = '{{.BasePublicSharesURL}}' + "/" + encodeURIComponent(shareID);
  215. if (shareScope == '1') {
  216. $('#expiredShare').hide();
  217. $('#writeShare').hide();
  218. $('#readShare').show();
  219. $('#readLink').attr("href", shareURL + "/download");
  220. $('#readLink').attr("title", shareURL + "/download");
  221. $('#readLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/download");
  222. $('#readUncompressedLink').attr("href", shareURL + "/download?compress=false");
  223. $('#readUncompressedLink').attr("title", shareURL + "/download?compress=false");
  224. $('#readUncompressedLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/download?compress=false");
  225. $('#readBrowseLink').attr("href", shareURL + "/browse");
  226. $('#readBrowseLink').attr("title", shareURL + "/browse");
  227. $('#readBrowseLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/browse");
  228. } else {
  229. $('#expiredShare').hide();
  230. $('#writeShare').show();
  231. $('#readShare').hide();
  232. $('#writePageLink').attr("href", shareURL + "/upload");
  233. $('#writePageLink').attr("title", shareURL + "/upload");
  234. $('#writePageLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/upload");
  235. }
  236. }
  237. $('#link_modal').modal('show');
  238. }
  239. const tableData = [];
  240. {{- range .Shares}}
  241. tableData.push(['{{.Name}}','{{.Scope}}','{{- if .Password}}1{{- else}}0{{- end}}','{{.ShareID}}','{{- if .IsExpired}}1{{- else}}0{{- end}}', '{{.ExpiresAt}}', '{{.LastUseAt}}', '{{.UsedTokens}}', '{{.MaxTokens}}']);
  242. {{- end}}
  243. var sharesDatatable = function(){
  244. var dt;
  245. var initDatatable = function () {
  246. dt = $('#dataTable').DataTable({
  247. data: tableData,
  248. columnDefs: [
  249. {
  250. target: 0,
  251. render: function(data, type, row) {
  252. if (type === 'display') {
  253. return escapeHTML(data);
  254. }
  255. return data;
  256. }
  257. },
  258. {
  259. target: 1,
  260. render: function (data, type, row) {
  261. if (type === 'display') {
  262. switch (data){
  263. case "2":
  264. return $.t('share.scope_write');
  265. case "3":
  266. return $.t('share.scope_read_write');
  267. default:
  268. return $.t('share.scope_read');
  269. }
  270. }
  271. return data;
  272. }
  273. },
  274. {
  275. target: 2,
  276. searchable: false,
  277. orderable: false,
  278. render: function (data, type, row) {
  279. if (type === 'display') {
  280. let info = "";
  281. if (row[5] > 0){
  282. info+= $.t('share.expiration_date', {
  283. val: parseInt(row[5], 10),
  284. formatParams: {
  285. val: { year: 'numeric', month: 'numeric', day: 'numeric' },
  286. }
  287. });
  288. }
  289. if (row[6] > 0){
  290. info+= $.t('share.last_use', {
  291. val: parseInt(row[6], 10),
  292. formatParams: {
  293. val: { year: 'numeric', month: 'numeric', day: 'numeric' },
  294. }
  295. });
  296. }
  297. if (row[8] > 0){
  298. info+= $.t('share.usage', {used: row[7], total: row[8]})
  299. } else {
  300. info+= $.t('share.used_tokens', {used: row[7]})
  301. }
  302. if (data == "1"){
  303. info+= $.t('share.password_protected')
  304. }
  305. return info;
  306. }
  307. return data;
  308. }
  309. },
  310. {
  311. targets: 3,
  312. searchable: false,
  313. orderable: false,
  314. className: 'text-end',
  315. render: function (data, type, row) {
  316. if (type === 'display') {
  317. return `<div class="d-flex justify-content-end">
  318. <div class="ms-2">
  319. <a href="#" class="btn btn-sm btn-icon btn-light btn-active-light-primary" data-share-table-action="show_link">
  320. <i class="ki-duotone ki-fasten fs-5 m-0">
  321. <span class="path1"></span>
  322. <span class="path2"></span>
  323. </i>
  324. </a>
  325. </div>
  326. <div class="ms-2">
  327. <button type="button" class="btn btn-sm btn-icon btn-light btn-active-light-primary"
  328. data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
  329. <i class="ki-duotone ki-dots-square fs-5 m-0">
  330. <span class="path1"></span>
  331. <span class="path2"></span>
  332. <span class="path3"></span>
  333. <span class="path4"></span>
  334. </i>
  335. </button>
  336. <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-600 menu-state-bg-light-primary fw-semibold fs-7 w-150px py-4" data-kt-menu="true">
  337. <div class="menu-item px-3">
  338. <a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a>
  339. </div>
  340. <div class="menu-item px-3">
  341. <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a>
  342. </div>
  343. </div>
  344. </div>
  345. </div>`;
  346. }
  347. return "";
  348. }
  349. }
  350. ],
  351. deferRender: true,
  352. stateSave: true,
  353. stateDuration: 0,
  354. stateLoadParams: function (settings, data) {
  355. if (data.search.search){
  356. const filterSearch = document.querySelector('[data-share-table-filter="search"]');
  357. filterSearch.value = data.search.search;
  358. }
  359. },
  360. language: {
  361. info: $.t('datatable.info'),
  362. infoEmpty: $.t('datatable.info_empty'),
  363. infoFiltered: $.t('datatable.info_filtered'),
  364. loadingRecords: "",
  365. processing: $.t('datatable.processing'),
  366. zeroRecords: "",
  367. emptyTable: $.t('share.no_share')
  368. },
  369. order: [[1, 'asc']],
  370. initComplete: function(settings, json) {
  371. $('#loader').addClass("d-none");
  372. $('#card_content').removeClass("d-none");
  373. let api = $.fn.dataTable.Api(settings);
  374. api.columns.adjust().draw("page");
  375. KTMenu.createInstances();
  376. handleRowActions();
  377. $('#table_body').localize();
  378. }
  379. });
  380. dt.on('draw', function () {
  381. KTMenu.createInstances();
  382. handleRowActions();
  383. $('#table_body').localize();
  384. });
  385. }
  386. var handleSearchDatatable = function () {
  387. const filterSearch = document.querySelector('[data-share-table-filter="search"]');
  388. filterSearch.addEventListener('keyup', function (e) {
  389. dt.rows().deselect();
  390. dt.search(e.target.value, true, false).draw();
  391. });
  392. }
  393. function handleRowActions() {
  394. const editButtons = document.querySelectorAll('[data-share-table-action="edit_row"]');
  395. editButtons.forEach(d => {
  396. d.addEventListener("click", function(e){
  397. e.preventDefault();
  398. const parent = e.target.closest('tr');
  399. editAction(dt.row(parent).data()[3]);
  400. });
  401. });
  402. const deleteButtons = document.querySelectorAll('[data-share-table-action="delete_row"]');
  403. deleteButtons.forEach(d => {
  404. d.addEventListener("click", function(e){
  405. e.preventDefault();
  406. const parent = e.target.closest('tr');
  407. deleteAction(dt.row(parent).data()[3]);
  408. });
  409. });
  410. const showLinkButtons = document.querySelectorAll('[data-share-table-action="show_link"]');
  411. showLinkButtons.forEach(d => {
  412. d.addEventListener("click", function(e){
  413. e.preventDefault();
  414. let rowData = dt.row(e.target.closest('tr')).data();
  415. showShareLink(rowData[3], rowData[1], rowData[4]);
  416. });
  417. });
  418. }
  419. return {
  420. init: function () {
  421. initDatatable();
  422. handleSearchDatatable();
  423. }
  424. }
  425. }();
  426. $(document).on("i18nshow", function(){
  427. sharesDatatable.init();
  428. var clipboard = new ClipboardJS('.btn-clipboard-copy',{
  429. container: document.getElementById('link_modal')
  430. });
  431. clipboard.on('success', function (e) {
  432. e.trigger.querySelectorAll('span').forEach(spanEl => {
  433. if (spanEl.getAttribute('data-i18n')){
  434. setI18NData($(spanEl),"general.copied");
  435. setTimeout(function(){
  436. setI18NData($(spanEl),"general.copy_link");
  437. }, 3000)
  438. }
  439. });
  440. e.clearSelection();
  441. });
  442. });
  443. </script>
  444. {{end}}