file_properties.html 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title locale="title/title">File Properties</title>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
  7. <link rel="stylesheet" href="../../script/semantic/semantic.min.css">
  8. <script type="text/javascript" src="../../script/jquery.min.js"></script>
  9. <script type="text/javascript" src="../../script/ao_module.js"></script>
  10. <script type="text/javascript" src="../../script/applocale.js"></script>
  11. <link rel="stylesheet" href="dialog.css">
  12. <script type="text/javascript" src="js/dialog.js"></script>
  13. </head>
  14. <body class="whiteTheme">
  15. <div class="dialog">
  16. <div class="dlgTabs">
  17. <div class="dlgTab active" tab="basic" onclick="switchTab('basic');" locale="tab/basic">Basic</div>
  18. <div class="dlgTab" tab="permission" onclick="switchTab('permission');" locale="tab/permission">Permissions</div>
  19. <div class="dlgTab" tab="versions" onclick="switchTab('versions');" locale="tab/versions">Versions</div>
  20. <div class="dlgTab" tab="storage" id="storageTab" style="display:none;" onclick="switchTab('storage');" locale="tab/storage">Storage</div>
  21. </div>
  22. <div class="dialogBody">
  23. <!-- Basic -->
  24. <div class="dlgPage active" id="page_basic">
  25. <div class="dlgFileHead">
  26. <div id="basicIcon"></div>
  27. <div class="dlgFileHeadInfo">
  28. <input id="basicName" class="dlgField" type="text" value="" readonly>
  29. <div class="dlgMime" id="basicMime"></div>
  30. </div>
  31. </div>
  32. <div id="basicProperties"></div>
  33. </div>
  34. <!-- Permissions -->
  35. <div class="dlgPage" id="page_permission">
  36. <div id="permissionNotice" class="dlgNotice" style="display:none;">
  37. <i class="info circle icon"></i>
  38. <span id="permissionNoticeText"></span>
  39. </div>
  40. <div id="permissionForm">
  41. <div class="dlgFormRow">
  42. <label locale="perm/owner">Owner</label>
  43. <div class="control">
  44. <div class="staticValue"><i class="user icon"></i> <span id="permOwnerName">-</span></div>
  45. </div>
  46. </div>
  47. <div class="dlgFormRow">
  48. <label locale="perm/group">Group</label>
  49. <div class="control">
  50. <div class="dlgSelectWrap">
  51. <select id="permGroup" class="dlgField" disabled></select>
  52. </div>
  53. </div>
  54. </div>
  55. <div class="dlgSectionLabel" locale="perm/access">Access Permissions</div>
  56. <div class="dlgFormRow">
  57. <label locale="perm/owner">Owner</label>
  58. <div class="control">
  59. <div class="dlgSelectWrap">
  60. <select id="permUser" class="dlgField" onchange="syncFromSelectors();"></select>
  61. </div>
  62. </div>
  63. <div class="suffix" id="permUserBits">(---)</div>
  64. </div>
  65. <div class="dlgFormRow">
  66. <label locale="perm/group">Group</label>
  67. <div class="control">
  68. <div class="dlgSelectWrap">
  69. <select id="permGrp" class="dlgField" onchange="syncFromSelectors();"></select>
  70. </div>
  71. </div>
  72. <div class="suffix" id="permGrpBits">(---)</div>
  73. </div>
  74. <div class="dlgFormRow">
  75. <label locale="perm/others">Others</label>
  76. <div class="control">
  77. <div class="dlgSelectWrap">
  78. <select id="permOther" class="dlgField" onchange="syncFromSelectors();"></select>
  79. </div>
  80. </div>
  81. <div class="suffix" id="permOtherBits">(---)</div>
  82. </div>
  83. <div class="dlgDivider"></div>
  84. <div class="dlgFormRow">
  85. <label locale="perm/octal">Octal</label>
  86. <div class="control" style="max-width: 100px;">
  87. <input id="permOctal" class="dlgField" type="text" maxlength="4" oninput="syncFromOctal();">
  88. </div>
  89. </div>
  90. <div class="dlgFormRow" id="recursiveRow" title="Recursive permission change is not supported by this server">
  91. <label style="flex: 1 1 auto;" locale="perm/recursive">Apply to enclosed files and folders</label>
  92. <div class="dlgToggle">
  93. <input id="permRecursive" type="checkbox" disabled>
  94. <label class="slider" for="permRecursive"></label>
  95. </div>
  96. </div>
  97. </div>
  98. </div>
  99. <!-- Versions -->
  100. <div class="dlgPage" id="page_versions">
  101. <div id="versionsUnavailable" class="dlgEmpty" style="display:none;">
  102. <i class="history icon"></i>
  103. <div locale="message/noVersions">Version history is only kept for files</div>
  104. </div>
  105. <div id="versionsManager" style="display:none;">
  106. <div class="dlgNotice">
  107. <i class="info circle icon"></i>
  108. <span locale="versions/desc">Any file version that is more than 30 days old is removed from the system automatically.</span>
  109. </div>
  110. <button class="dlgBtn" onclick="createSnapshot();">
  111. <i class="plus icon"></i> <span locale="versions/newver">Create New Version</span>
  112. </button>
  113. <div class="dlgSectionLabel" locale="versions/history">Version History</div>
  114. <div class="dlgList" id="versionList"></div>
  115. </div>
  116. </div>
  117. <!-- Storage: where the file physically is, for drives that know -->
  118. <div class="dlgPage" id="page_storage">
  119. <div id="storageFields"></div>
  120. <div id="storageCopiesSection" style="display:none;">
  121. <div class="dlgSectionLabel" id="storageCopiesLabel" locale="storage/copies">File Copies</div>
  122. <div id="storageCopies"></div>
  123. </div>
  124. </div>
  125. </div>
  126. <div class="dialogFooter">
  127. <button class="dlgBtn footerBasic" onclick="copyPath();"><i class="copy outline icon"></i> <span locale="button/copyPath">Copy Path</span></button>
  128. <button id="deleteAllVersionsBtn" class="dlgBtn danger footerVersions" style="display:none;" onclick="deleteAllVersions();">
  129. <i class="trash icon"></i> <span locale="versions/removeAll">Delete All Versions</span>
  130. </button>
  131. <div class="spacer"></div>
  132. <button class="dlgBtn footerBasic footerVersions footerStorage" onclick="ao_module_close();" locale="button/close">Close</button>
  133. <button class="dlgBtn footerPermission" style="display:none;" onclick="ao_module_close();" locale="button/cancel">Cancel</button>
  134. <button id="permApplyBtn" class="dlgBtn primary footerPermission" style="display:none;" onclick="applyPermission();" locale="button/confirm">OK</button>
  135. </div>
  136. </div>
  137. <div class="dlgMenu" id="rowMenu"></div>
  138. <div id="dlgLoader" class="dlgLoader">
  139. <div class="dlgSpinner"></div>
  140. <div locale="loader/loadingFileSize">Calculating File Size</div>
  141. </div>
  142. <script>
  143. /*
  144. file_properties.html
  145. Three tabs over one selection: everything the object is -
  146. type, size, opener, paths, dates and ownership (Basic), who may
  147. touch it (Permissions) and its snapshot history (Versions). The
  148. tab strip replaced a single scrolling table that mixed all
  149. three, plus two buttons that opened further windows.
  150. Basic groups its rows by kind - what / opens with / where /
  151. when / who - because a flat list of ten rows reads as one
  152. undifferentiated block however the rows are ordered.
  153. Versions is the whole snapshot manager, which used to be a
  154. separate window reached through a button in here. Nothing about
  155. it wanted its own window: it acts on the one file this dialog
  156. is already about.
  157. Permissions are a POSIX concept: on a Windows host the tab is
  158. still readable but visibly out of reach, because the server
  159. refuses chmod there.
  160. */
  161. var files = ao_module_loadInputFiles();
  162. var fileInfo = {}; //Properties of the single selected object
  163. var multiProperties = []; //Properties of every object, multi selection only
  164. var hostOS = "";
  165. var permissionSupported = true;
  166. var permissionLoaded = false;
  167. var currentTab = "basic";
  168. //Storage tab state, filled in only by a drive that reports one
  169. var storageInfo = null;
  170. //Version manager state
  171. var versionsLoaded = false;
  172. var versionList = [];
  173. var versionPollTimer = null;
  174. //Octal digit to a human readable access level. Index is the digit.
  175. var permissionLevels = [
  176. {bits: "---", key: "perm/level/none", text: "None"},
  177. {bits: "--x", key: "perm/level/execute", text: "Execute only"},
  178. {bits: "-w-", key: "perm/level/write", text: "Write only"},
  179. {bits: "-wx", key: "perm/level/writeExecute", text: "Write and execute"},
  180. {bits: "r--", key: "perm/level/read", text: "Read only"},
  181. {bits: "r-x", key: "perm/level/readExecute", text: "Read and execute"},
  182. {bits: "rw-", key: "perm/level/readWrite", text: "Read and write"},
  183. {bits: "rwx", key: "perm/level/readWriteExecute", text: "Read, write and execute"}
  184. ];
  185. $(document).on("click", function(event){
  186. if ($(event.target).closest("#rowMenu, .dlgIconBtn").length == 0){
  187. $("#rowMenu").hide();
  188. }
  189. });
  190. applocale.init("../locale/file_properties.json", function(){
  191. applocale.translate();
  192. buildPermissionSelectors();
  193. dlgInitTheme();
  194. loadHostInfo();
  195. initFileProperties();
  196. loadStorageInfo();
  197. });
  198. /* ---------------------------------------------------------------
  199. Tabs
  200. --------------------------------------------------------------- */
  201. function switchTab(tabName){
  202. currentTab = tabName;
  203. $(".dlgTab").removeClass("active");
  204. $('.dlgTab[tab="' + tabName + '"]').addClass("active");
  205. $(".dlgPage").removeClass("active");
  206. $("#page_" + tabName).addClass("active");
  207. $(".footerBasic, .footerVersions, .footerPermission, .footerStorage").hide();
  208. if (tabName == "permission"){
  209. $(".footerPermission").show();
  210. if (!permissionLoaded){
  211. loadPermission();
  212. }
  213. }else if (tabName == "storage"){
  214. $(".footerStorage").show();
  215. }else if (tabName == "versions"){
  216. $(".footerVersions").show();
  217. if (!versionsSupported()){
  218. //Nothing to delete where no history is kept
  219. $("#deleteAllVersionsBtn").hide();
  220. }else if (!versionsLoaded){
  221. versionsLoaded = true;
  222. loadVersionHistory();
  223. }
  224. }else{
  225. $(".footerBasic").show();
  226. }
  227. //Another window may snapshot the same file, but only watch for
  228. //that while the list is actually on screen
  229. $("#rowMenu").hide();
  230. if (versionPollTimer != null){
  231. clearInterval(versionPollTimer);
  232. versionPollTimer = null;
  233. }
  234. if (tabName == "versions" && versionsSupported()){
  235. versionPollTimer = setInterval(checkHistoryListUpdate, 3000);
  236. }
  237. }
  238. /* ---------------------------------------------------------------
  239. Host capabilities
  240. --------------------------------------------------------------- */
  241. function loadHostInfo(){
  242. $.get("../../system/info/getArOZInfo", function(data){
  243. hostOS = data.HostOS;
  244. if (hostOS == "windows"){
  245. //The server rejects chmod on a Windows host, so the
  246. //whole tab is shown read only rather than pretending
  247. permissionSupported = false;
  248. $('.dlgTab[tab="permission"]').addClass("unsupported");
  249. $("#permissionNoticeText").text(applocale.getString("perm/unsupported/windows",
  250. "This server runs on Windows, which does not use POSIX file permissions. The values below are shown for reference only."));
  251. $("#permissionNotice").show();
  252. $("#permissionForm").addClass("dlgUnsupported");
  253. $("#permApplyBtn").addClass("disabled");
  254. }
  255. });
  256. }
  257. /* ---------------------------------------------------------------
  258. Basic and Versions tabs
  259. --------------------------------------------------------------- */
  260. function initFileProperties(){
  261. if (files.length == 1){
  262. getFileProp(files[0], renderSingleObject);
  263. }else if (files.length > 1){
  264. multiProperties = [];
  265. for (var i = 0; i < files.length; i++){
  266. getFileProp(files[i], function(data){
  267. multiProperties.push(data);
  268. if (multiProperties.length == files.length){
  269. renderMultipleObjects();
  270. }
  271. });
  272. }
  273. }else{
  274. hideLoader();
  275. $("#basicProperties").html('<div class="dlgEmpty"><i class="file outline icon"></i><div>' +
  276. applocale.getString("message/nofile", "No file selected") + "</div></div>");
  277. }
  278. }
  279. function getFileProp(vpath, callback){
  280. $.ajax({
  281. url: "../../system/file_system/getProperties",
  282. data: {path: vpath},
  283. method: "POST",
  284. success: function(data){
  285. callback(data);
  286. },
  287. error: function(){
  288. callback({error: "Unable to reach the file system"});
  289. }
  290. });
  291. }
  292. function propRow(key, value, dim){
  293. return '<div class="dlgPropRow"><span class="k">' + dlgEscape(key) + '</span>' +
  294. '<span class="v' + (dim ? " dim" : "") + '">' + value + "</span></div>";
  295. }
  296. function localizedKey(key){
  297. return applocale.getString("properties/key/" + key, key);
  298. }
  299. //Rows of the same kind travel together; the gap between groups is
  300. //what keeps a ten row list scannable
  301. function propGroup(rows){
  302. var kept = rows.filter(function(row){ return row != ""; });
  303. if (kept.length == 0){
  304. return "";
  305. }
  306. return '<div class="dlgPropGroup">' + kept.join("") + "</div>";
  307. }
  308. function renderSingleObject(data){
  309. hideLoader();
  310. fileInfo = data;
  311. if (data.error !== undefined){
  312. $("#basicProperties").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
  313. dlgEscape(data.error) + "</div></div>");
  314. $('.dlgTab[tab="permission"]').addClass("unsupported");
  315. return;
  316. }
  317. var isShortcut = (!data.IsDirectory && data.Basename.split(".").pop() == "shortcut");
  318. //Heading
  319. $("#basicIcon").html(dlgFileIconHTML(data.Basename, data.IsDirectory));
  320. $("#basicName").val(data.Basename);
  321. $("#basicMime").text(data.MimeType);
  322. loadThumbnail(data);
  323. //Basic property rows
  324. var fileTypeText = applocale.getString("type/file", "File");
  325. if (data.IsDirectory){
  326. fileTypeText = applocale.getString("type/folder", "Folder");
  327. }else if (isShortcut){
  328. fileTypeText = applocale.getString("type/shortcut", "System Shortcut");
  329. }
  330. //A real file is the only thing that has an opener to show
  331. var hasOpener = (!data.IsDirectory && !isShortcut);
  332. var basicHTML = "";
  333. //What it is
  334. basicHTML += propGroup([
  335. propRow(localizedKey("File Type"), dlgEscape(fileTypeText)),
  336. propRow(data.IsDirectory ? localizedKey("Folder Size") : localizedKey("File Size"), sizeText(data.Filesize))
  337. ]);
  338. //What opens it
  339. basicHTML += propGroup([
  340. hasOpener ? '<div id="defaultOpenerRow">' + propRow(localizedKey("Default WebApp"),
  341. '<span class="dim">' + applocale.getString("message/loading", "Loading...") + "</span>") + "</div>" : ""
  342. ]);
  343. //Where it lives
  344. basicHTML += propGroup([
  345. propRow(localizedKey("Location"), dlgEscape(data.VirtualDirname)),
  346. propRow(localizedKey("Virtual Path"), dlgEscape(data.VirtualPath)),
  347. propRow(localizedKey("Storage Path"), dlgEscape(data.StoragePath))
  348. ]);
  349. //When it changed
  350. basicHTML += propGroup([
  351. propRow(localizedKey("Last Modified"), modTimeText(data)),
  352. propRow(localizedKey("Created"), creationTimeText(data))
  353. ]);
  354. //Who it belongs to
  355. basicHTML += propGroup([
  356. propRow(localizedKey("Owner"), dlgEscape(data.Owner)),
  357. propRow(localizedKey("Permission"), '<span style="font-family: Consolas, Menlo, monospace;">' + dlgEscape(data.Permission) + "</span>")
  358. ]);
  359. $("#basicProperties").html(basicHTML);
  360. if (hasOpener){
  361. loadDefaultOpener(data);
  362. }
  363. //Versions tab
  364. renderVersionsTab(data, isShortcut);
  365. //Permission tab defaults
  366. $("#permOwnerName").text(data.Owner);
  367. loadUserGroups(data.Owner);
  368. }
  369. function renderMultipleObjects(){
  370. hideLoader();
  371. var totalSize = 0;
  372. var fileCount = 0;
  373. var folderCount = 0;
  374. var readable = [];
  375. for (var i = 0; i < multiProperties.length; i++){
  376. if (multiProperties[i].error !== undefined){
  377. continue;
  378. }
  379. readable.push(multiProperties[i]);
  380. totalSize += multiProperties[i].Filesize;
  381. if (multiProperties[i].IsDirectory){
  382. folderCount++;
  383. }else{
  384. fileCount++;
  385. }
  386. }
  387. if (readable.length == 0){
  388. $("#basicProperties").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
  389. applocale.getString("message/unreadable", "The selected items could not be read") + "</div></div>");
  390. return;
  391. }
  392. fileInfo = readable[0];
  393. $("#basicIcon").html(dlgMultiIconHTML());
  394. $("#basicName").val(readable.length + applocale.getString("counter/items", " items selected"));
  395. $("#basicMime").text(applocale.getString("selection/multi", "Multiple selections"));
  396. var basicHTML = "";
  397. basicHTML += propGroup([
  398. propRow(localizedKey("File Type"), dlgEscape(
  399. fileCount + applocale.getString("counter/files", " Files") + ", " +
  400. folderCount + applocale.getString("counter/folders", " Folders"))),
  401. propRow(localizedKey("Total Size"), sizeText(totalSize))
  402. ]);
  403. basicHTML += propGroup([
  404. propRow(localizedKey("Location"), dlgEscape(readable[0].VirtualDirname)),
  405. propRow(localizedKey("Virtual Directory"), dlgEscape(readable[0].VirtualDirname + "/")),
  406. propRow(localizedKey("Storage Directory"), dlgEscape(readable[0].StorageDirname + "/"))
  407. ]);
  408. basicHTML += propGroup([
  409. propRow(localizedKey("Owner"), dlgEscape(readable[0].Owner))
  410. ]);
  411. $("#basicProperties").html(basicHTML);
  412. //Version history is per file, so a selection has none to show
  413. renderVersionsTab(null, false);
  414. $("#permOwnerName").text(readable[0].Owner);
  415. loadUserGroups(readable[0].Owner);
  416. }
  417. /* ---------------------------------------------------------------
  418. Versions tab
  419. Snapshots are kept per file under .metadata/.localver, so a
  420. folder, a shortcut or a multiple selection has nothing to show
  421. and the tab greys out instead of offering actions that would
  422. fail.
  423. --------------------------------------------------------------- */
  424. function versionsSupported(){
  425. return (files.length == 1 && fileInfo.error === undefined &&
  426. fileInfo.IsDirectory === false && !isShortcutFile(fileInfo));
  427. }
  428. function isShortcutFile(data){
  429. return (data != undefined && data.Basename != undefined &&
  430. data.Basename.split(".").pop() == "shortcut");
  431. }
  432. function renderVersionsTab(data, isShortcut){
  433. if (data == null || data.IsDirectory || isShortcut){
  434. $("#versionsUnavailable").show();
  435. $("#versionsManager").hide();
  436. $("#deleteAllVersionsBtn").hide();
  437. $('.dlgTab[tab="versions"]').addClass("unsupported");
  438. return;
  439. }
  440. $("#versionsUnavailable").hide();
  441. $("#versionsManager").show();
  442. $("#versionList").html('<div class="dlgEmpty"><div class="dlgSpinner" style="margin: 0px auto 10px auto;"></div><div>' +
  443. applocale.getString("message/loading", "Loading...") + "</div></div>");
  444. //This tab may already be the one on screen when the properties
  445. //finally arrive, in which case nothing else starts the load
  446. if (currentTab == "versions" && !versionsLoaded){
  447. versionsLoaded = true;
  448. loadVersionHistory();
  449. }
  450. }
  451. function loadVersionHistory(){
  452. $.ajax({
  453. url: "../../system/file_system/versionHistory",
  454. data: {path: files[0]},
  455. method: "POST",
  456. success: function(data){
  457. if (data == undefined || data.error !== undefined){
  458. $("#versionList").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
  459. dlgEscape(data == undefined ? "No response" : data.error) + "</div></div>");
  460. return;
  461. }
  462. versionList = data.Versions;
  463. renderVersionList();
  464. },
  465. error: function(){
  466. $("#versionList").html('<div class="dlgEmpty"><i class="question circle outline icon"></i><div>' +
  467. applocale.getString("versions/failed", "Unable to read the version history") + "</div></div>");
  468. }
  469. });
  470. }
  471. function checkHistoryListUpdate(){
  472. $.ajax({
  473. url: "../../system/file_system/versionHistory",
  474. data: {path: files[0]},
  475. method: "POST",
  476. success: function(data){
  477. if (data != undefined && data.error == undefined && versionList.length != data.Versions.length){
  478. versionList = data.Versions;
  479. renderVersionList();
  480. }
  481. }
  482. });
  483. }
  484. function renderVersionList(){
  485. var html = "";
  486. //The file as it stands now is the newest version. Numbers are
  487. //assigned here, not by the server: a snapshot is named by its
  488. //timestamp, which says nothing about the order on screen.
  489. html += versionRowHTML({
  490. number: versionList.length + 1,
  491. current: true,
  492. time: fileInfo.LastModTime,
  493. size: fileInfo.Filesize,
  494. historyID: "",
  495. relpath: ""
  496. });
  497. //Server returns newest first, so the number counts down
  498. for (var i = 0; i < versionList.length; i++){
  499. html += versionRowHTML({
  500. number: versionList.length - i,
  501. current: false,
  502. time: versionList[i].OverwriteTime,
  503. size: versionList[i].Filesize,
  504. historyID: versionList[i].HistoryID,
  505. relpath: versionList[i].Relpath
  506. });
  507. }
  508. $("#versionList").html(html);
  509. $("#deleteAllVersionsBtn").toggleClass("disabled", versionList.length == 0);
  510. if (versionList.length == 0){
  511. $("#versionList").append('<div class="dlgEmpty"><div>' +
  512. applocale.getString("versions/empty/desc", "This file has no previous version histories.") + "</div></div>");
  513. }
  514. }
  515. function versionRowHTML(entry){
  516. var title = applocale.getString("versions/label", "Version") + " " + entry.number;
  517. if (entry.current){
  518. title += " " + applocale.getString("versions/current", "(current)");
  519. }
  520. var owner = (fileInfo.Owner == undefined || fileInfo.Owner == "") ? "" : " (" + fileInfo.Owner + ")";
  521. return '<div class="dlgListRow static' + (entry.current ? " selected" : "") + '">' +
  522. '<div class="rowIcon"><i class="file outline icon"></i></div>' +
  523. '<div class="rowText">' +
  524. '<div class="rowTitle">' + dlgEscape(title) + "</div>" +
  525. '<div class="rowSub">' + dlgEscape(entry.time + owner) + "</div>" +
  526. "</div>" +
  527. '<div class="rowMeta">' + dlgEscape(dlgBytesToSize(entry.size)) + "</div>" +
  528. '<button class="dlgIconBtn" verid="' + dlgEscape(entry.historyID) + '" relpath="' + dlgEscape(entry.relpath) + '" ' +
  529. 'iscurrent="' + (entry.current ? "true" : "false") + '" onclick="openRowMenu(this);">' +
  530. '<i class="ellipsis horizontal icon"></i></button>' +
  531. "</div>";
  532. }
  533. function openRowMenu(button){
  534. var isCurrent = $(button).attr("iscurrent") == "true";
  535. var versionID = $(button).attr("verid");
  536. var relpath = $(button).attr("relpath");
  537. var html = '<div class="dlgMenuItem" onclick="downloadVersion(\'' + dlgEscape(relpath) + '\');">' +
  538. '<i class="download icon"></i>' + applocale.getString("versions/action/download", "Download Version") + "</div>";
  539. if (!isCurrent){
  540. html += '<div class="dlgMenuItem" onclick="restoreVersion(\'' + dlgEscape(versionID) + '\');">' +
  541. '<i class="history icon"></i>' + applocale.getString("versions/action/restore", "Restore This Version") + "</div>";
  542. html += '<div class="dlgMenuItem danger" onclick="deleteVersion(\'' + dlgEscape(versionID) + '\');">' +
  543. '<i class="trash icon"></i>' + applocale.getString("versions/action/delete", "Delete") + "</div>";
  544. }
  545. var menu = $("#rowMenu");
  546. menu.html(html).show();
  547. //Anchor under the button, kept inside the window
  548. var offset = $(button).offset();
  549. var left = offset.left + $(button).outerWidth() - menu.outerWidth();
  550. var top = offset.top + $(button).outerHeight() + 4;
  551. if (left < 8){
  552. left = 8;
  553. }
  554. if (top + menu.outerHeight() > $(window).height() - 8){
  555. top = offset.top - menu.outerHeight() - 4;
  556. }
  557. menu.css({left: left + "px", top: top + "px"});
  558. }
  559. function downloadVersion(relpath){
  560. $("#rowMenu").hide();
  561. var dirname = files[0].split("/");
  562. var basename = dirname.pop();
  563. var accessPath = dirname.join("/") + "/" + (relpath == "" ? basename : relpath);
  564. window.open("../../media/download?file=" + accessPath);
  565. }
  566. function createSnapshot(){
  567. $.ajax({
  568. url: "../../system/file_system/versionHistory",
  569. data: {path: files[0], opr: "new"},
  570. method: "POST",
  571. success: function(data){
  572. if (data != undefined && data.error != undefined){
  573. dlgToast(data.error);
  574. }else{
  575. dlgToast(applocale.getString("versions/msg/created", "New Version Created"));
  576. loadVersionHistory();
  577. }
  578. }
  579. });
  580. }
  581. function restoreVersion(versionID){
  582. $("#rowMenu").hide();
  583. if (!confirm(applocale.getString("versions/warn/restore", "Restoring this file will remove all newer version histories of this file. Confirm?"))){
  584. return;
  585. }
  586. $.ajax({
  587. url: "../../system/file_system/versionHistory",
  588. data: {path: files[0], opr: "restore", histid: versionID},
  589. method: "POST",
  590. success: function(data){
  591. if (data != undefined && data.error != undefined){
  592. dlgToast(data.error);
  593. return;
  594. }
  595. dlgToast(applocale.getString("versions/msg/restored", "Restore Succeed"));
  596. //The file itself changed, so the Basic tab is stale too
  597. refreshAfterRestore();
  598. }
  599. });
  600. }
  601. //A restore rewrites the file, so the size and modification time on
  602. //the Basic tab are stale the moment it succeeds
  603. function refreshAfterRestore(){
  604. getFileProp(files[0], function(data){
  605. if (data.error === undefined){
  606. renderSingleObject(data);
  607. }
  608. loadVersionHistory();
  609. });
  610. }
  611. function deleteVersion(versionID){
  612. $("#rowMenu").hide();
  613. $.ajax({
  614. url: "../../system/file_system/versionHistory",
  615. data: {path: files[0], opr: "delete", histid: versionID},
  616. method: "POST",
  617. success: function(data){
  618. if (data != undefined && data.error != undefined){
  619. dlgToast(data.error);
  620. }else{
  621. loadVersionHistory();
  622. }
  623. }
  624. });
  625. }
  626. function deleteAllVersions(){
  627. if (!confirm(applocale.getString("versions/warn/deleteall", "This operation will PERMANENTLY DELETE ALL version history of this file. Confirm?"))){
  628. return;
  629. }
  630. $.ajax({
  631. url: "../../system/file_system/versionHistory",
  632. data: {path: files[0], opr: "deleteAll"},
  633. method: "POST",
  634. success: function(data){
  635. if (data != undefined && data.error != undefined){
  636. dlgToast(data.error);
  637. }else{
  638. dlgToast(applocale.getString("versions/msg/deletedall", "All version history deleted"));
  639. loadVersionHistory();
  640. }
  641. }
  642. });
  643. }
  644. function loadDefaultOpener(data){
  645. $.ajax({
  646. url: "../../system/modules/getDefault",
  647. method: "GET",
  648. data: {
  649. opr: "launch",
  650. ext: "." + data.Basename.split(".").pop(),
  651. mode: "launch"
  652. },
  653. success: function(openerinfo){
  654. var value = "";
  655. if (openerinfo == undefined || openerinfo.Name == undefined){
  656. value = '<a href="#" onclick="changeDefaultWebApp(); return false;">' +
  657. applocale.getString("message/setDefault", "Set Default WebApp") + "</a>";
  658. }else{
  659. var iconPath = (openerinfo.IconPath == "") ? "img/system/favicon.png" : openerinfo.IconPath;
  660. value = '<span style="display: inline-flex; align-items: center; gap: 8px;">' +
  661. '<img src="../../' + dlgEscape(iconPath) + '" style="height: 20px; width: 20px; object-fit: contain;">' +
  662. dlgEscape(openerinfo.Name) +
  663. '<a href="#" style="margin-left: 4px;" onclick="changeDefaultWebApp(); return false;">' +
  664. applocale.getString("button/change", "Change") + "</a></span>";
  665. }
  666. $("#defaultOpenerRow").html(propRow(localizedKey("Default WebApp"), value));
  667. }
  668. });
  669. }
  670. //A thumbnail says more about an image than any glyph can, but most
  671. //file types have none: the drawn icon stays until one arrives
  672. function loadThumbnail(data){
  673. if (data.IsDirectory){
  674. return;
  675. }
  676. fetch("../../system/file_system/loadThumbnail?vpath=" + encodeURIComponent(data.VirtualPath))
  677. .then(function(response){ return response.json(); })
  678. .then(function(imageData){
  679. if (imageData == undefined || imageData.error !== undefined || String(imageData).trim() == ""){
  680. return;
  681. }
  682. $("#basicIcon").find(".dlgFileIcon").html('<img class="thumbnail" src="data:image/jpg;base64,' + imageData + '">');
  683. })
  684. .catch(function(){
  685. //Keep the drawn icon
  686. });
  687. }
  688. function sizeText(bytes){
  689. if (bytes < 0){
  690. //Network drives do not report a size
  691. return '<span class="dim">' + applocale.getString("properties/error/Not available for network folders", "Not available for network folders") + "</span>";
  692. }
  693. return dlgEscape(dlgBytesToSize(bytes) + " (" + bytes + " bytes)");
  694. }
  695. function modTimeText(data){
  696. if (data.LastModUnix == 0){
  697. return '<span class="dim">' + applocale.getString("properties/error/No record", "No record") + "</span>";
  698. }
  699. return dlgEscape(data.LastModTime);
  700. }
  701. //Creation time is not recorded by every platform: Linux only
  702. //exposes it through statx, so the server may report it as unknown
  703. function creationTimeText(data){
  704. if (data.CreationTime == undefined || data.CreationTime == ""){
  705. return '<span class="dim">' + applocale.getString("properties/error/Not recorded by host", "Not recorded by this host") + "</span>";
  706. }
  707. return dlgEscape(data.CreationTime);
  708. }
  709. function hideLoader(){
  710. $("#dlgLoader").hide();
  711. }
  712. /* ---------------------------------------------------------------
  713. Storage tab
  714. Most drives have nothing to add: the storage path on the Basic
  715. tab is the whole answer. A drive whose files live somewhere
  716. else - cluster:/ keeps every file on one or more other nodes -
  717. reports where through its file system abstraction, and this
  718. tab appears to show it. Nothing here is cluster specific: the
  719. server sends rows and locations, whatever kind of drive
  720. resolved them.
  721. --------------------------------------------------------------- */
  722. function loadStorageInfo(){
  723. if (files.length != 1){
  724. //A selection has no single place to point at
  725. return;
  726. }
  727. $.ajax({
  728. url: "../../system/file_system/getStorageInfo",
  729. data: {path: files[0]},
  730. method: "POST",
  731. success: function(data){
  732. if (data == undefined || data.error !== undefined){
  733. //This drive does not report one. Leave the tab out
  734. return;
  735. }
  736. storageInfo = data;
  737. renderStorageTab(data);
  738. $("#storageTab").show();
  739. }
  740. });
  741. }
  742. //Keys and the wordy half of values are translated where the locale
  743. //file has them and shown as they came otherwise, so a drive can
  744. //report a node name or a hash that no locale can know about
  745. function storageKey(key){
  746. return applocale.getString("storage/key/" + key, key);
  747. }
  748. function storageValue(value, args){
  749. var text = applocale.getString("storage/value/" + value, value);
  750. if (args != undefined){
  751. for (var i = 0; i < args.length; i++){
  752. text = text.split("{" + i + "}").join(
  753. applocale.getString("storage/value/" + args[i], args[i]));
  754. }
  755. }
  756. return text;
  757. }
  758. function storageFieldRows(fields){
  759. var rows = [];
  760. for (var i = 0; i < fields.length; i++){
  761. if (fields[i].value == ""){
  762. continue;
  763. }
  764. rows.push(propRow(storageKey(fields[i].key),
  765. dlgEscape(storageValue(fields[i].value, fields[i].args))));
  766. }
  767. return rows;
  768. }
  769. function renderStorageTab(data){
  770. //The tab is named after whatever answered, e.g. Cluster
  771. $("#storageTab").text(applocale.getString("storage/title/" + data.type, data.title));
  772. $("#storageFields").html(propGroup(storageFieldRows(data.fields || [])));
  773. var items = data.items || [];
  774. if (items.length == 0){
  775. //A folder has no copies of its own, and a file that has
  776. //just been written may not have one recorded yet
  777. $("#storageCopiesSection").hide();
  778. return;
  779. }
  780. var html = "";
  781. for (var i = 0; i < items.length; i++){
  782. html += storageCopyHTML(items[i]);
  783. }
  784. $("#storageCopies").html(html);
  785. $("#storageCopiesSection").show();
  786. }
  787. function storageCopyHTML(item){
  788. var state = item.state;
  789. if (state != "ok" && state != "pending" && state != "stale" && state != "error"){
  790. state = "unknown";
  791. }
  792. var head = '<div class="copyHead">' +
  793. '<span class="copyDot ' + state + '"></span>' +
  794. '<span class="copyName">' + dlgEscape(item.title) + "</span>";
  795. if (item.local){
  796. //Whether the file is on the host being used right now is
  797. //the first thing anyone opening this tab wants to know
  798. head += '<span class="copyBadge">' +
  799. dlgEscape(applocale.getString("storage/thisNode", "This node")) + "</span>";
  800. }
  801. if (item.status != undefined && item.status != ""){
  802. head += '<span class="copyStatus ' + state + '">' +
  803. dlgEscape(storageValue(item.status)) + "</span>";
  804. }
  805. head += "</div>";
  806. var sub = "";
  807. if (item.subtitle != undefined && item.subtitle != ""){
  808. sub = '<div class="copySub">' + dlgEscape(item.subtitle) + "</div>";
  809. }
  810. return '<div class="dlgCopy">' + head + sub +
  811. storageFieldRows(item.fields || []).join("") + "</div>";
  812. }
  813. /* ---------------------------------------------------------------
  814. Permission tab
  815. --------------------------------------------------------------- */
  816. function buildPermissionSelectors(){
  817. var options = "";
  818. for (var i = 7; i >= 0; i--){
  819. options += '<option value="' + i + '">' +
  820. dlgEscape(applocale.getString(permissionLevels[i].key, permissionLevels[i].text)) + "</option>";
  821. }
  822. $("#permUser").html(options);
  823. $("#permGrp").html(options);
  824. $("#permOther").html(options);
  825. }
  826. /*
  827. The file's POSIX group is not something ArozOS can change - the
  828. server only accepts a mode string - so the field lists the
  829. owner's permission groups for reference and stays disabled.
  830. */
  831. function loadUserGroups(owner){
  832. $.get("../../system/users/userinfo", function(data){
  833. var groups = [];
  834. if (data != undefined && data.Usergroup != undefined && data.Username == owner){
  835. groups = data.Usergroup;
  836. }
  837. if (groups.length == 0){
  838. groups = [applocale.getString("perm/group/unknown", "Not available")];
  839. }
  840. var options = "";
  841. for (var i = 0; i < groups.length; i++){
  842. options += '<option value="' + dlgEscape(groups[i]) + '">' + dlgEscape(groups[i]) + "</option>";
  843. }
  844. $("#permGroup").html(options);
  845. }).fail(function(){
  846. $("#permGroup").html('<option>' + applocale.getString("perm/group/unknown", "Not available") + "</option>");
  847. });
  848. }
  849. function loadPermission(){
  850. if (files.length == 0){
  851. return;
  852. }
  853. permissionLoaded = true;
  854. $.ajax({
  855. url: "../../system/file_system/handleFilePermission",
  856. data: {file: files[0]},
  857. success: function(data){
  858. if (data == undefined || data.error !== undefined){
  859. setOctal("0644");
  860. return;
  861. }
  862. setOctal(String(data));
  863. },
  864. error: function(){
  865. setOctal("0644");
  866. }
  867. });
  868. }
  869. function setOctal(octal){
  870. //The server reports a four digit mode where the first digit is
  871. //always zero; only the last three are editable here
  872. var mode = String(octal).replace(/[^0-7]/g, "");
  873. while (mode.length < 4){
  874. mode = "0" + mode;
  875. }
  876. mode = mode.substring(mode.length - 4);
  877. $("#permOctal").val(mode);
  878. $("#permUser").val(mode.charAt(1));
  879. $("#permGrp").val(mode.charAt(2));
  880. $("#permOther").val(mode.charAt(3));
  881. updateBitsHint();
  882. }
  883. function syncFromSelectors(){
  884. var mode = "0" + $("#permUser").val() + $("#permGrp").val() + $("#permOther").val();
  885. $("#permOctal").val(mode);
  886. updateBitsHint();
  887. }
  888. function syncFromOctal(){
  889. var typed = $("#permOctal").val().replace(/[^0-7]/g, "");
  890. $("#permOctal").val(typed);
  891. if (typed.length != 4){
  892. return;
  893. }
  894. $("#permUser").val(typed.charAt(1));
  895. $("#permGrp").val(typed.charAt(2));
  896. $("#permOther").val(typed.charAt(3));
  897. updateBitsHint();
  898. }
  899. function updateBitsHint(){
  900. $("#permUserBits").text("(" + permissionLevels[parseInt($("#permUser").val())].bits + ")");
  901. $("#permGrpBits").text("(" + permissionLevels[parseInt($("#permGrp").val())].bits + ")");
  902. $("#permOtherBits").text("(" + permissionLevels[parseInt($("#permOther").val())].bits + ")");
  903. }
  904. function applyPermission(){
  905. if (!permissionSupported){
  906. return;
  907. }
  908. var mode = $("#permOctal").val();
  909. if (mode.length != 4 || mode.charAt(0) != "0"){
  910. dlgToast(applocale.getString("perm/invalid", "Invalid permission value"));
  911. return;
  912. }
  913. var pending = files.length;
  914. var failure = "";
  915. files.forEach(function(filepath){
  916. $.ajax({
  917. url: "../../system/file_system/handleFilePermission",
  918. data: {file: filepath, mode: mode},
  919. success: function(data){
  920. if (data != undefined && data.error !== undefined){
  921. failure = data.error;
  922. }
  923. },
  924. error: function(){
  925. failure = applocale.getString("perm/failed", "Unable to update the file permission");
  926. },
  927. complete: function(){
  928. pending--;
  929. if (pending == 0){
  930. if (failure != ""){
  931. dlgToast(failure);
  932. }else{
  933. dlgToast(applocale.getString("perm/updated", "Permission updated"));
  934. permissionLoaded = false;
  935. }
  936. }
  937. }
  938. });
  939. });
  940. }
  941. /* ---------------------------------------------------------------
  942. Footer actions
  943. --------------------------------------------------------------- */
  944. function copyPath(){
  945. var path = fileInfo.VirtualPath;
  946. if (path == undefined && files.length > 0){
  947. path = files[0];
  948. }
  949. if (path == undefined){
  950. return;
  951. }
  952. if (dlgCopyToClipboard(path)){
  953. dlgToast(applocale.getString("message/copied", "Path copied"));
  954. }else{
  955. dlgToast(applocale.getString("message/copyFailed", "Unable to copy the path"));
  956. }
  957. }
  958. //Open the opener picker for this file type
  959. function changeDefaultWebApp(){
  960. var openFileObject = {
  961. filepath: fileInfo.VirtualPath,
  962. filename: fileInfo.Basename
  963. };
  964. ao_module_newfw({
  965. url: "SystemAO/file_system/defaultOpener.html#" + encodeURIComponent(JSON.stringify(openFileObject)),
  966. width: 380,
  967. height: 560,
  968. appicon: "SystemAO/file_system/img/opener.png",
  969. title: "Default WebApp for " + fileInfo.Ext,
  970. parent: ao_module_windowID,
  971. callback: "handleRefresh"
  972. });
  973. }
  974. //Called back by the opener picker once a new default is stored
  975. function handleRefresh(){
  976. if (fileInfo.VirtualPath != undefined){
  977. loadDefaultOpener(fileInfo);
  978. }
  979. }
  980. </script>
  981. </body>
  982. </html>