shares.html 24 KB

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