app.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  1. // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
  2. // All rights reserved. Use of this source code is governed by an MIT-style
  3. // license that can be found in the LICENSE file.
  4. /*jslint browser: true, continue: true, plusplus: true */
  5. /*global $: false, angular: false, console: false, validLangs: false */
  6. 'use strict';
  7. var syncthing = angular.module('syncthing', ['pascalprecht.translate']);
  8. var urlbase = 'rest';
  9. var requestTimeout = 300000; // Retry the REST request after 5 minutes without reply
  10. var refreshTimeout = 5000; // Do not refresh the generic status faster than 5 seconds
  11. var refreshQuickTimeout = 500; // Do not refresh the quick stats faster than 500 milliseconds
  12. syncthing.config(function ($httpProvider, $translateProvider) {
  13. $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
  14. $httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
  15. $translateProvider.useStaticFilesLoader({
  16. prefix: 'lang/lang-',
  17. suffix: '.json'
  18. });
  19. });
  20. syncthing.controller('EventCtrl', function ($scope, $http) {
  21. $scope.lastEvent = null;
  22. var lastID = 0;
  23. var successFn = function (data) {
  24. // When Syncthing restarts while the long polling connection is in
  25. // progress the browser on some platforms returns a 200 (since the
  26. // headers has been flushed with the return code 200), with no data.
  27. // This basically means that the connection has been reset, and the call
  28. // was not actually sucessful.
  29. if (!data) {
  30. errorFn(data);
  31. return;
  32. }
  33. $scope.$emit('UIOnline');
  34. if (lastID > 0) {
  35. data.forEach(function (event) {
  36. console.log("event", event.id, event.type, event.data);
  37. $scope.$emit(event.type, event);
  38. });
  39. }
  40. $scope.lastEvent = data[data.length - 1];
  41. lastID = $scope.lastEvent.id;
  42. setTimeout(function () {
  43. $http.get(urlbase + '/events?since=' + lastID)
  44. .success(successFn)
  45. .error(errorFn);
  46. }, 500);
  47. };
  48. var errorFn = function (data) {
  49. $scope.$emit('UIOffline');
  50. setTimeout(function () {
  51. $http.get(urlbase + '/events?limit=1')
  52. .success(successFn)
  53. .error(errorFn);
  54. }, 1000);
  55. };
  56. $http.get(urlbase + '/events?limit=1')
  57. .success(successFn)
  58. .error(errorFn);
  59. });
  60. syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
  61. var prevDate = 0;
  62. var getOK = true;
  63. var navigatingAway = false;
  64. var online = false;
  65. var restarting = false;
  66. $scope.completion = {};
  67. $scope.config = {};
  68. $scope.configInSync = true;
  69. $scope.connections = {};
  70. $scope.errors = [];
  71. $scope.model = {};
  72. $scope.myID = '';
  73. $scope.nodes = [];
  74. $scope.protocolChanged = false;
  75. $scope.reportData = {};
  76. $scope.reportPreview = false;
  77. $scope.repos = {};
  78. $scope.seenError = '';
  79. $scope.upgradeInfo = {};
  80. $scope.stats = {};
  81. $http.get(urlbase + "/lang").success(function (langs) {
  82. // Find the first language in the list provided by the user's browser
  83. // that is a prefix of a language we have available. That is, "en"
  84. // sent by the browser will match "en" or "en-US", while "zh-TW" will
  85. // match only "zh-TW" and not "zh-CN".
  86. var lang, matching;
  87. for (var i = 0; i < langs.length; i++) {
  88. lang = langs[i];
  89. if (lang.length < 2) {
  90. continue;
  91. }
  92. matching = validLangs.filter(function (possibleLang) {
  93. // The langs returned by the /rest/langs call will be in lower
  94. // case. We compare to the lowercase version of the language
  95. // code we have as well.
  96. possibleLang = possibleLang.toLowerCase();
  97. if (possibleLang.length > lang.length) {
  98. return possibleLang.indexOf(lang) === 0;
  99. } else {
  100. return lang.indexOf(possibleLang) === 0;
  101. }
  102. });
  103. if (matching.length >= 1) {
  104. $translate.use(matching[0]);
  105. return;
  106. }
  107. }
  108. // Fallback if nothing matched
  109. $translate.use("en");
  110. });
  111. $(window).bind('beforeunload', function () {
  112. navigatingAway = true;
  113. });
  114. $scope.$on("$locationChangeSuccess", function () {
  115. var lang = $location.search().lang;
  116. if (lang) {
  117. $translate.use(lang);
  118. }
  119. });
  120. $scope.needActions = {
  121. 'rm': 'Del',
  122. 'rmdir': 'Del (dir)',
  123. 'sync': 'Sync',
  124. 'touch': 'Update',
  125. };
  126. $scope.needIcons = {
  127. 'rm': 'remove',
  128. 'rmdir': 'remove',
  129. 'sync': 'download',
  130. 'touch': 'asterisk',
  131. };
  132. $scope.$on('UIOnline', function (event, arg) {
  133. if (online && !restarting) {
  134. return;
  135. }
  136. if (restarting) {
  137. document.location.reload(true);
  138. } else {
  139. console.log('UIOnline');
  140. $scope.init();
  141. online = true;
  142. restarting = false;
  143. $('#networkError').modal('hide');
  144. $('#restarting').modal('hide');
  145. $('#shutdown').modal('hide');
  146. }
  147. });
  148. $scope.$on('UIOffline', function (event, arg) {
  149. if (navigatingAway || !online) {
  150. return;
  151. }
  152. console.log('UIOffline');
  153. online = false;
  154. if (!restarting) {
  155. $('#networkError').modal();
  156. }
  157. });
  158. $scope.$on('StateChanged', function (event, arg) {
  159. var data = arg.data;
  160. if ($scope.model[data.repo]) {
  161. $scope.model[data.repo].state = data.to;
  162. }
  163. });
  164. $scope.$on('LocalIndexUpdated', function (event, arg) {
  165. var data = arg.data;
  166. refreshRepo(data.repo);
  167. // Update completion status for all nodes that we share this repo with.
  168. $scope.repos[data.repo].Nodes.forEach(function (nodeCfg) {
  169. refreshCompletion(nodeCfg.NodeID, data.repo);
  170. });
  171. });
  172. $scope.$on('RemoteIndexUpdated', function (event, arg) {
  173. var data = arg.data;
  174. refreshRepo(data.repo);
  175. refreshCompletion(data.node, data.repo);
  176. });
  177. $scope.$on('NodeDisconnected', function (event, arg) {
  178. delete $scope.connections[arg.data.id];
  179. refreshNodeStats();
  180. });
  181. $scope.$on('NodeConnected', function (event, arg) {
  182. if (!$scope.connections[arg.data.id]) {
  183. $scope.connections[arg.data.id] = {
  184. inbps: 0,
  185. outbps: 0,
  186. InBytesTotal: 0,
  187. OutBytesTotal: 0,
  188. Address: arg.data.addr,
  189. };
  190. $scope.completion[arg.data.id] = {
  191. _total: 100,
  192. };
  193. }
  194. });
  195. $scope.$on('ConfigLoaded', function (event) {
  196. if ($scope.config.Options.URAccepted === 0) {
  197. // If usage reporting has been neither accepted nor declined,
  198. // we want to ask the user to make a choice. But we don't want
  199. // to bug them during initial setup, so we set a cookie with
  200. // the time of the first visit. When that cookie is present
  201. // and the time is more than four hours ago, we ask the
  202. // question.
  203. var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  204. if (!firstVisit) {
  205. document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
  206. } else {
  207. if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
  208. $('#ur').modal();
  209. }
  210. }
  211. }
  212. });
  213. $scope.$on('ConfigSaved', function (event, arg) {
  214. updateLocalConfig(arg.data);
  215. $http.get(urlbase + '/config/sync').success(function (data) {
  216. $scope.configInSync = data.configInSync;
  217. });
  218. });
  219. var nextCall = {};
  220. function refreshRepo(repo) {
  221. var key = "refreshRepo" + repo;
  222. if (nextCall[key] && nextCall[key] > Date.now()) {
  223. return;
  224. }
  225. nextCall[key] = Date.now() + requestTimeout;
  226. $http.get(urlbase + '/model?repo=' + encodeURIComponent(repo)).success(function (data) {
  227. nextCall[key] = Date.now() + refreshTimeout;
  228. $scope.model[repo] = data;
  229. console.log("refreshRepo", repo, data);
  230. });
  231. }
  232. function updateLocalConfig(config) {
  233. var hasConfig = !isEmptyObject($scope.config);
  234. $scope.config = config;
  235. $scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
  236. $scope.nodes = $scope.config.Nodes;
  237. $scope.nodes.forEach(function (nodeCfg) {
  238. $scope.completion[nodeCfg.NodeID] = {
  239. _total: 100,
  240. };
  241. });
  242. $scope.nodes.sort(nodeCompare);
  243. $scope.repos = repoMap($scope.config.Repositories);
  244. Object.keys($scope.repos).forEach(function (repo) {
  245. refreshRepo(repo);
  246. $scope.repos[repo].Nodes.forEach(function (nodeCfg) {
  247. refreshCompletion(nodeCfg.NodeID, repo);
  248. });
  249. });
  250. if (!hasConfig) {
  251. $scope.$emit('ConfigLoaded');
  252. }
  253. }
  254. function refreshSystem() {
  255. var key = "refreshSystem"
  256. if (nextCall[key] && nextCall[key] > Date.now()) {
  257. return;
  258. }
  259. nextCall[key] = Date.now() + requestTimeout;
  260. $http.get(urlbase + '/system').success(function (data) {
  261. nextCall[key] = Date.now() + refreshQuickTimeout;
  262. $scope.myID = data.myID;
  263. $scope.system = data;
  264. console.log("refreshSystem", data);
  265. });
  266. }
  267. function refreshCompletion(node, repo) {
  268. if (node === $scope.myID) {
  269. return;
  270. }
  271. var key = "refreshCompletion" + node + repo;
  272. if (nextCall[key] && nextCall[key] > Date.now()) {
  273. return;
  274. }
  275. nextCall[key] = Date.now() + requestTimeout;
  276. $http.get(urlbase + '/completion?node=' + node + '&repo=' + encodeURIComponent(repo)).success(function (data) {
  277. nextCall[key] = Date.now() + refreshTimeout;
  278. if (!$scope.completion[node]) {
  279. $scope.completion[node] = {};
  280. }
  281. $scope.completion[node][repo] = data.completion;
  282. var tot = 0,
  283. cnt = 0;
  284. for (var cmp in $scope.completion[node]) {
  285. if (cmp === "_total") {
  286. continue;
  287. }
  288. tot += $scope.completion[node][cmp];
  289. cnt += 1;
  290. }
  291. $scope.completion[node]._total = tot / cnt;
  292. console.log("refreshCompletion", node, repo, $scope.completion[node]);
  293. });
  294. }
  295. function refreshConnectionStats() {
  296. var key = "refreshConnectionStats"
  297. if (nextCall[key] && nextCall[key] > Date.now()) {
  298. return;
  299. }
  300. nextCall[key] = Date.now() + requestTimeout;
  301. $http.get(urlbase + '/connections').success(function (data) {
  302. nextCall[key] = Date.now() + refreshQuickTimeout;
  303. var now = Date.now(),
  304. td = (now - prevDate) / 1000,
  305. id;
  306. prevDate = now;
  307. for (id in data) {
  308. if (!data.hasOwnProperty(id)) {
  309. continue;
  310. }
  311. try {
  312. data[id].inbps = Math.max(0, 8 * (data[id].InBytesTotal - $scope.connections[id].InBytesTotal) / td);
  313. data[id].outbps = Math.max(0, 8 * (data[id].OutBytesTotal - $scope.connections[id].OutBytesTotal) / td);
  314. } catch (e) {
  315. data[id].inbps = 0;
  316. data[id].outbps = 0;
  317. }
  318. }
  319. $scope.connections = data;
  320. console.log("refreshConnections", data);
  321. });
  322. }
  323. function refreshErrors() {
  324. var key = "refreshErrors"
  325. if (nextCall[key] && nextCall[key] > Date.now()) {
  326. return;
  327. }
  328. nextCall[key] = Date.now() + requestTimeout;
  329. $http.get(urlbase + '/errors').success(function (data) {
  330. nextCall[key] = Date.now() + refreshQuickTimeout;
  331. $scope.errors = data.errors;
  332. console.log("refreshErrors", data);
  333. });
  334. }
  335. function refreshConfig() {
  336. $http.get(urlbase + '/config').success(function (data) {
  337. updateLocalConfig(data);
  338. console.log("refreshConfig", data);
  339. });
  340. $http.get(urlbase + '/config/sync').success(function (data) {
  341. $scope.configInSync = data.configInSync;
  342. });
  343. }
  344. var refreshNodeStats = function () {
  345. var key = "refreshNode";
  346. if (nextCall[key] && nextCall[key] > Date.now()) {
  347. return;
  348. }
  349. nextCall[key] = Date.now() + requestTimeout;
  350. $http.get(urlbase + "/stats/node").success(function (data) {
  351. nextCall[key] = Date.now() + refreshTimeout;
  352. $scope.stats = data;
  353. for (var node in $scope.stats) {
  354. $scope.stats[node].LastSeen = new Date($scope.stats[node].LastSeen);
  355. $scope.stats[node].LastSeenDays = (new Date() - $scope.stats[node].LastSeen) / 1000 / 86400;
  356. }
  357. console.log("refreshNodeStats", data);
  358. });
  359. }
  360. $scope.init = function () {
  361. refreshSystem();
  362. refreshConfig();
  363. refreshConnectionStats();
  364. refreshNodeStats();
  365. $http.get(urlbase + '/version').success(function (data) {
  366. $scope.version = data.version;
  367. });
  368. $http.get(urlbase + '/report').success(function (data) {
  369. $scope.reportData = data;
  370. });
  371. $http.get(urlbase + '/upgrade').success(function (data) {
  372. $scope.upgradeInfo = data;
  373. }).error(function () {
  374. $scope.upgradeInfo = {};
  375. });
  376. };
  377. $scope.refresh = function () {
  378. refreshSystem();
  379. refreshConnectionStats();
  380. refreshErrors();
  381. };
  382. $scope.repoStatus = function (repo) {
  383. if (typeof $scope.model[repo] === 'undefined') {
  384. return 'unknown';
  385. }
  386. if ($scope.model[repo].invalid !== '') {
  387. return 'stopped';
  388. }
  389. return '' + $scope.model[repo].state;
  390. };
  391. $scope.repoClass = function (repo) {
  392. if (typeof $scope.model[repo] === 'undefined') {
  393. return 'info';
  394. }
  395. if ($scope.model[repo].invalid !== '') {
  396. return 'danger';
  397. }
  398. var state = '' + $scope.model[repo].state;
  399. if (state == 'idle') {
  400. return 'success';
  401. }
  402. if (state == 'syncing') {
  403. return 'primary';
  404. }
  405. if (state == 'scanning') {
  406. return 'primary';
  407. }
  408. return 'info';
  409. };
  410. $scope.syncPercentage = function (repo) {
  411. if (typeof $scope.model[repo] === 'undefined') {
  412. return 100;
  413. }
  414. if ($scope.model[repo].globalBytes === 0) {
  415. return 100;
  416. }
  417. var pct = 100 * $scope.model[repo].inSyncBytes / $scope.model[repo].globalBytes;
  418. return Math.floor(pct);
  419. };
  420. $scope.nodeIcon = function (nodeCfg) {
  421. if ($scope.connections[nodeCfg.NodeID]) {
  422. if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
  423. return 'ok';
  424. } else {
  425. return 'refresh';
  426. }
  427. }
  428. return 'minus';
  429. };
  430. $scope.nodeClass = function (nodeCfg) {
  431. if ($scope.connections[nodeCfg.NodeID]) {
  432. if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
  433. return 'success';
  434. } else {
  435. return 'primary';
  436. }
  437. }
  438. return 'info';
  439. };
  440. $scope.nodeAddr = function (nodeCfg) {
  441. var conn = $scope.connections[nodeCfg.NodeID];
  442. if (conn) {
  443. return conn.Address;
  444. }
  445. return '?';
  446. };
  447. $scope.nodeCompletion = function (nodeCfg) {
  448. var conn = $scope.connections[nodeCfg.NodeID];
  449. if (conn) {
  450. return conn.Completion + '%';
  451. }
  452. return '';
  453. };
  454. $scope.findNode = function (nodeID) {
  455. var matches = $scope.nodes.filter(function (n) {
  456. return n.NodeID == nodeID;
  457. });
  458. if (matches.length != 1) {
  459. return undefined;
  460. }
  461. return matches[0];
  462. };
  463. $scope.nodeName = function (nodeCfg) {
  464. if (typeof nodeCfg === 'undefined') {
  465. return "";
  466. }
  467. if (nodeCfg.Name) {
  468. return nodeCfg.Name;
  469. }
  470. return nodeCfg.NodeID.substr(0, 6);
  471. };
  472. $scope.thisNodeName = function () {
  473. var node = $scope.thisNode();
  474. if (typeof node === 'undefined') {
  475. return "(unknown node)";
  476. }
  477. if (node.Name) {
  478. return node.Name;
  479. }
  480. return node.NodeID.substr(0, 6);
  481. };
  482. $scope.editSettings = function () {
  483. // Make a working copy
  484. $scope.tmpOptions = angular.copy($scope.config.Options);
  485. $scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
  486. $scope.tmpOptions.NodeName = $scope.thisNode().Name;
  487. $scope.tmpGUI = angular.copy($scope.config.GUI);
  488. $('#settings').modal();
  489. };
  490. $scope.saveConfig = function () {
  491. var cfg = JSON.stringify($scope.config);
  492. var opts = {
  493. headers: {
  494. 'Content-Type': 'application/json'
  495. }
  496. };
  497. $http.post(urlbase + '/config', cfg, opts).success(function () {
  498. $http.get(urlbase + '/config/sync').success(function (data) {
  499. $scope.configInSync = data.configInSync;
  500. });
  501. });
  502. };
  503. $scope.saveSettings = function () {
  504. // Make sure something changed
  505. var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) ||
  506. !angular.equals($scope.config.GUI, $scope.tmpGUI);
  507. if (changed) {
  508. // Check if usage reporting has been enabled or disabled
  509. if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
  510. $scope.tmpOptions.URAccepted = 1000;
  511. } else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0) {
  512. $scope.tmpOptions.URAccepted = -1;
  513. }
  514. // Check if protocol will need to be changed on restart
  515. if ($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS) {
  516. $scope.protocolChanged = true;
  517. }
  518. // Apply new settings locally
  519. $scope.thisNode().Name = $scope.tmpOptions.NodeName;
  520. $scope.config.Options = angular.copy($scope.tmpOptions);
  521. $scope.config.GUI = angular.copy($scope.tmpGUI);
  522. $scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) {
  523. return x.trim();
  524. });
  525. $scope.saveConfig();
  526. }
  527. $('#settings').modal("hide");
  528. };
  529. $scope.restart = function () {
  530. restarting = true;
  531. $('#restarting').modal();
  532. $http.post(urlbase + '/restart');
  533. $scope.configInSync = true;
  534. // Switch webpage protocol if needed
  535. if ($scope.protocolChanged) {
  536. var protocol = 'http';
  537. if ($scope.config.GUI.UseTLS) {
  538. protocol = 'https';
  539. }
  540. setTimeout(function () {
  541. window.location.protocol = protocol;
  542. }, 2500);
  543. $scope.protocolChanged = false;
  544. }
  545. };
  546. $scope.upgrade = function () {
  547. restarting = true;
  548. $('#upgrading').modal();
  549. $http.post(urlbase + '/upgrade').success(function () {
  550. $('#restarting').modal();
  551. $('#upgrading').modal('hide');
  552. }).error(function () {
  553. $('#upgrading').modal('hide');
  554. });
  555. };
  556. $scope.shutdown = function () {
  557. restarting = true;
  558. $http.post(urlbase + '/shutdown').success(function () {
  559. $('#shutdown').modal();
  560. });
  561. $scope.configInSync = true;
  562. };
  563. $scope.editNode = function (nodeCfg) {
  564. $scope.currentNode = $.extend({}, nodeCfg);
  565. $scope.editingExisting = true;
  566. $scope.editingSelf = (nodeCfg.NodeID == $scope.myID);
  567. $scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
  568. $scope.nodeEditor.$setPristine();
  569. $('#editNode').modal();
  570. };
  571. $scope.idNode = function () {
  572. $('#idqr').modal('show');
  573. };
  574. $scope.addNode = function () {
  575. $scope.currentNode = {
  576. AddressesStr: 'dynamic',
  577. Compression: true
  578. };
  579. $scope.editingExisting = false;
  580. $scope.editingSelf = false;
  581. $scope.nodeEditor.$setPristine();
  582. $('#editNode').modal();
  583. };
  584. $scope.deleteNode = function () {
  585. $('#editNode').modal('hide');
  586. if (!$scope.editingExisting) {
  587. return;
  588. }
  589. $scope.nodes = $scope.nodes.filter(function (n) {
  590. return n.NodeID !== $scope.currentNode.NodeID;
  591. });
  592. $scope.config.Nodes = $scope.nodes;
  593. for (var id in $scope.repos) {
  594. $scope.repos[id].Nodes = $scope.repos[id].Nodes.filter(function (n) {
  595. return n.NodeID !== $scope.currentNode.NodeID;
  596. });
  597. }
  598. $scope.saveConfig();
  599. };
  600. $scope.saveNode = function () {
  601. var nodeCfg, done, i;
  602. $('#editNode').modal('hide');
  603. nodeCfg = $scope.currentNode;
  604. nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) {
  605. return x.trim();
  606. });
  607. done = false;
  608. for (i = 0; i < $scope.nodes.length; i++) {
  609. if ($scope.nodes[i].NodeID === nodeCfg.NodeID) {
  610. $scope.nodes[i] = nodeCfg;
  611. done = true;
  612. break;
  613. }
  614. }
  615. if (!done) {
  616. $scope.nodes.push(nodeCfg);
  617. }
  618. $scope.nodes.sort(nodeCompare);
  619. $scope.config.Nodes = $scope.nodes;
  620. $scope.saveConfig();
  621. };
  622. $scope.otherNodes = function () {
  623. return $scope.nodes.filter(function (n) {
  624. return n.NodeID !== $scope.myID;
  625. });
  626. };
  627. $scope.thisNode = function () {
  628. var i, n;
  629. for (i = 0; i < $scope.nodes.length; i++) {
  630. n = $scope.nodes[i];
  631. if (n.NodeID === $scope.myID) {
  632. return n;
  633. }
  634. }
  635. };
  636. $scope.allNodes = function () {
  637. var nodes = $scope.otherNodes();
  638. nodes.push($scope.thisNode());
  639. return nodes;
  640. };
  641. $scope.errorList = function () {
  642. return $scope.errors.filter(function (e) {
  643. return e.Time > $scope.seenError;
  644. });
  645. };
  646. $scope.clearErrors = function () {
  647. $scope.seenError = $scope.errors[$scope.errors.length - 1].Time;
  648. $http.post(urlbase + '/error/clear');
  649. };
  650. $scope.friendlyNodes = function (str) {
  651. for (var i = 0; i < $scope.nodes.length; i++) {
  652. var cfg = $scope.nodes[i];
  653. str = str.replace(cfg.NodeID, $scope.nodeName(cfg));
  654. }
  655. return str;
  656. };
  657. $scope.repoList = function () {
  658. return repoList($scope.repos);
  659. };
  660. $scope.editRepo = function (nodeCfg) {
  661. $scope.currentRepo = angular.copy(nodeCfg);
  662. $scope.currentRepo.selectedNodes = {};
  663. $scope.currentRepo.Nodes.forEach(function (n) {
  664. $scope.currentRepo.selectedNodes[n.NodeID] = true;
  665. });
  666. if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "simple") {
  667. $scope.currentRepo.simpleFileVersioning = true;
  668. $scope.currentRepo.FileVersioningSelector = "simple";
  669. $scope.currentRepo.simpleKeep = +$scope.currentRepo.Versioning.Params.keep;
  670. } else if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "staggered") {
  671. $scope.currentRepo.staggeredFileVersioning = true;
  672. $scope.currentRepo.FileVersioningSelector = "staggered";
  673. $scope.currentRepo.staggeredMaxAge = Math.floor(+$scope.currentRepo.Versioning.Params.maxAge / 86400);
  674. $scope.currentRepo.staggeredCleanInterval = +$scope.currentRepo.Versioning.Params.cleanInterval;
  675. $scope.currentRepo.staggeredVersionsPath = $scope.currentRepo.Versioning.Params.versionsPath;
  676. } else {
  677. $scope.currentRepo.FileVersioningSelector = "none";
  678. }
  679. $scope.currentRepo.simpleKeep = $scope.currentRepo.simpleKeep || 5;
  680. $scope.currentRepo.staggeredCleanInterval = $scope.currentRepo.staggeredCleanInterval || 3600;
  681. $scope.currentRepo.staggeredVersionsPath = $scope.currentRepo.staggeredVersionsPath || "";
  682. // staggeredMaxAge can validly be zero, which we should not replace
  683. // with the default value of 365. So only set the default if it's
  684. // actually undefined.
  685. if (typeof $scope.currentRepo.staggeredMaxAge === 'undefined') {
  686. $scope.currentRepo.staggeredMaxAge = 365;
  687. }
  688. $scope.editingExisting = true;
  689. $scope.repoEditor.$setPristine();
  690. $('#editRepo').modal();
  691. };
  692. $scope.addRepo = function () {
  693. $scope.currentRepo = {
  694. selectedNodes: {}
  695. };
  696. $scope.currentRepo.RescanIntervalS = 60;
  697. $scope.currentRepo.FileVersioningSelector = "none";
  698. $scope.currentRepo.simpleKeep = 5;
  699. $scope.currentRepo.staggeredMaxAge = 365;
  700. $scope.currentRepo.staggeredCleanInterval = 3600;
  701. $scope.currentRepo.staggeredVersionsPath = "";
  702. $scope.editingExisting = false;
  703. $scope.repoEditor.$setPristine();
  704. $('#editRepo').modal();
  705. };
  706. $scope.saveRepo = function () {
  707. var repoCfg, done, i;
  708. $('#editRepo').modal('hide');
  709. repoCfg = $scope.currentRepo;
  710. repoCfg.Nodes = [];
  711. repoCfg.selectedNodes[$scope.myID] = true;
  712. for (var nodeID in repoCfg.selectedNodes) {
  713. if (repoCfg.selectedNodes[nodeID] === true) {
  714. repoCfg.Nodes.push({
  715. NodeID: nodeID
  716. });
  717. }
  718. }
  719. delete repoCfg.selectedNodes;
  720. if (repoCfg.FileVersioningSelector === "simple") {
  721. repoCfg.Versioning = {
  722. 'Type': 'simple',
  723. 'Params': {
  724. 'keep': '' + repoCfg.simpleKeep,
  725. }
  726. };
  727. delete repoCfg.simpleFileVersioning;
  728. delete repoCfg.simpleKeep;
  729. } else if (repoCfg.FileVersioningSelector === "staggered") {
  730. repoCfg.Versioning = {
  731. 'Type': 'staggered',
  732. 'Params': {
  733. 'maxAge': '' + (repoCfg.staggeredMaxAge * 86400),
  734. 'cleanInterval': '' + repoCfg.staggeredCleanInterval,
  735. 'versionsPath': '' + repoCfg.staggeredVersionsPath,
  736. }
  737. };
  738. delete repoCfg.staggeredFileVersioning;
  739. delete repoCfg.staggeredMaxAge;
  740. delete repoCfg.staggeredCleanInterval;
  741. delete repoCfg.staggeredVersionsPath;
  742. } else {
  743. delete repoCfg.Versioning;
  744. }
  745. $scope.repos[repoCfg.ID] = repoCfg;
  746. $scope.config.Repositories = repoList($scope.repos);
  747. $scope.saveConfig();
  748. };
  749. $scope.sharesRepo = function (repoCfg) {
  750. var names = [];
  751. repoCfg.Nodes.forEach(function (node) {
  752. names.push($scope.nodeName($scope.findNode(node.NodeID)));
  753. });
  754. names.sort();
  755. return names.join(", ");
  756. };
  757. $scope.deleteRepo = function () {
  758. $('#editRepo').modal('hide');
  759. if (!$scope.editingExisting) {
  760. return;
  761. }
  762. delete $scope.repos[$scope.currentRepo.ID];
  763. $scope.config.Repositories = repoList($scope.repos);
  764. $scope.saveConfig();
  765. };
  766. $scope.editIgnores = function () {
  767. if (!$scope.editingExisting) {
  768. return;
  769. }
  770. $('#editIgnoresButton').attr('disabled', 'disabled');
  771. $http.get(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID))
  772. .success(function (data) {
  773. data.ignore = data.ignore || [];
  774. $('#editRepo').modal('hide');
  775. var textArea = $('#editIgnores textarea');
  776. textArea.val(data.ignore.join('\n'));
  777. $('#editIgnores').modal()
  778. .on('hidden.bs.modal', function () {
  779. $('#editRepo').modal();
  780. })
  781. .on('shown.bs.modal', function () {
  782. textArea.focus();
  783. });
  784. })
  785. .then(function () {
  786. $('#editIgnoresButton').removeAttr('disabled');
  787. });
  788. };
  789. $scope.saveIgnores = function () {
  790. if (!$scope.editingExisting) {
  791. return;
  792. }
  793. $http.post(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID), {
  794. ignore: $('#editIgnores textarea').val().split('\n')
  795. });
  796. };
  797. $scope.setAPIKey = function (cfg) {
  798. cfg.APIKey = randomString(30, 32);
  799. };
  800. $scope.showURPreview = function () {
  801. $('#settings').modal('hide');
  802. $('#urPreview').modal().on('hidden.bs.modal', function () {
  803. $('#settings').modal();
  804. });
  805. };
  806. $scope.acceptUR = function () {
  807. $scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
  808. $scope.saveConfig();
  809. $('#ur').modal('hide');
  810. };
  811. $scope.declineUR = function () {
  812. $scope.config.Options.URAccepted = -1;
  813. $scope.saveConfig();
  814. $('#ur').modal('hide');
  815. };
  816. $scope.showNeed = function (repo) {
  817. $scope.neededLoaded = false;
  818. $('#needed').modal();
  819. $http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
  820. $scope.needed = data;
  821. $scope.neededLoaded = true;
  822. });
  823. };
  824. $scope.needAction = function (file) {
  825. var fDelete = 4096;
  826. var fDirectory = 16384;
  827. if ((file.Flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
  828. return 'rmdir';
  829. } else if ((file.Flags & fDelete) === fDelete) {
  830. return 'rm';
  831. } else if ((file.Flags & fDirectory) === fDirectory) {
  832. return 'touch';
  833. } else {
  834. return 'sync';
  835. }
  836. };
  837. $scope.override = function (repo) {
  838. $http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo));
  839. };
  840. $scope.about = function () {
  841. $('#about').modal('show');
  842. };
  843. $scope.showReportPreview = function () {
  844. $scope.reportPreview = true;
  845. };
  846. $scope.rescanRepo = function (repo) {
  847. $http.post(urlbase + "/scan?repo=" + encodeURIComponent(repo));
  848. };
  849. $scope.init();
  850. setInterval($scope.refresh, 10000);
  851. });
  852. function nodeCompare(a, b) {
  853. if (typeof a.Name !== 'undefined' && typeof b.Name !== 'undefined') {
  854. if (a.Name < b.Name)
  855. return -1;
  856. return a.Name > b.Name;
  857. }
  858. if (a.NodeID < b.NodeID) {
  859. return -1;
  860. }
  861. return a.NodeID > b.NodeID;
  862. }
  863. function repoCompare(a, b) {
  864. if (a.ID < b.ID) {
  865. return -1;
  866. }
  867. return a.ID > b.ID;
  868. }
  869. function repoMap(l) {
  870. var m = {};
  871. l.forEach(function (r) {
  872. m[r.ID] = r;
  873. });
  874. return m;
  875. }
  876. function repoList(m) {
  877. var l = [];
  878. for (var id in m) {
  879. l.push(m[id]);
  880. }
  881. l.sort(repoCompare);
  882. return l;
  883. }
  884. function decimals(val, num) {
  885. var digits, decs;
  886. if (val === 0) {
  887. return 0;
  888. }
  889. digits = Math.floor(Math.log(Math.abs(val)) / Math.log(10));
  890. decs = Math.max(0, num - digits);
  891. return decs;
  892. }
  893. function randomString(len, bits) {
  894. bits = bits || 36;
  895. var outStr = "",
  896. newStr;
  897. while (outStr.length < len) {
  898. newStr = Math.random().toString(bits).slice(2);
  899. outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
  900. }
  901. return outStr.toLowerCase();
  902. }
  903. function isEmptyObject(obj) {
  904. var name;
  905. for (name in obj) {
  906. return false;
  907. }
  908. return true;
  909. }
  910. syncthing.filter('natural', function () {
  911. return function (input, valid) {
  912. return input.toFixed(decimals(input, valid));
  913. };
  914. });
  915. syncthing.filter('binary', function () {
  916. return function (input) {
  917. if (input === undefined) {
  918. return '0 ';
  919. }
  920. if (input > 1024 * 1024 * 1024) {
  921. input /= 1024 * 1024 * 1024;
  922. return input.toFixed(decimals(input, 2)) + ' Gi';
  923. }
  924. if (input > 1024 * 1024) {
  925. input /= 1024 * 1024;
  926. return input.toFixed(decimals(input, 2)) + ' Mi';
  927. }
  928. if (input > 1024) {
  929. input /= 1024;
  930. return input.toFixed(decimals(input, 2)) + ' Ki';
  931. }
  932. return Math.round(input) + ' ';
  933. };
  934. });
  935. syncthing.filter('metric', function () {
  936. return function (input) {
  937. if (input === undefined) {
  938. return '0 ';
  939. }
  940. if (input > 1000 * 1000 * 1000) {
  941. input /= 1000 * 1000 * 1000;
  942. return input.toFixed(decimals(input, 2)) + ' G';
  943. }
  944. if (input > 1000 * 1000) {
  945. input /= 1000 * 1000;
  946. return input.toFixed(decimals(input, 2)) + ' M';
  947. }
  948. if (input > 1000) {
  949. input /= 1000;
  950. return input.toFixed(decimals(input, 2)) + ' k';
  951. }
  952. return Math.round(input) + ' ';
  953. };
  954. });
  955. syncthing.filter('alwaysNumber', function () {
  956. return function (input) {
  957. if (input === undefined) {
  958. return 0;
  959. }
  960. return input;
  961. };
  962. });
  963. syncthing.filter('basename', function () {
  964. return function (input) {
  965. if (input === undefined)
  966. return "";
  967. var parts = input.split(/[\/\\]/);
  968. if (!parts || parts.length < 1) {
  969. return input;
  970. }
  971. return parts[parts.length - 1];
  972. };
  973. });
  974. syncthing.directive('uniqueRepo', function () {
  975. return {
  976. require: 'ngModel',
  977. link: function (scope, elm, attrs, ctrl) {
  978. ctrl.$parsers.unshift(function (viewValue) {
  979. if (scope.editingExisting) {
  980. // we shouldn't validate
  981. ctrl.$setValidity('uniqueRepo', true);
  982. } else if (scope.repos[viewValue]) {
  983. // the repo exists already
  984. ctrl.$setValidity('uniqueRepo', false);
  985. } else {
  986. // the repo is unique
  987. ctrl.$setValidity('uniqueRepo', true);
  988. }
  989. return viewValue;
  990. });
  991. }
  992. };
  993. });
  994. syncthing.directive('validNodeid', function ($http) {
  995. return {
  996. require: 'ngModel',
  997. link: function (scope, elm, attrs, ctrl) {
  998. ctrl.$parsers.unshift(function (viewValue) {
  999. if (scope.editingExisting) {
  1000. // we shouldn't validate
  1001. ctrl.$setValidity('validNodeid', true);
  1002. } else {
  1003. $http.get(urlbase + '/nodeid?id=' + viewValue).success(function (resp) {
  1004. if (resp.error) {
  1005. ctrl.$setValidity('validNodeid', false);
  1006. } else {
  1007. ctrl.$setValidity('validNodeid', true);
  1008. }
  1009. });
  1010. }
  1011. return viewValue;
  1012. });
  1013. }
  1014. };
  1015. });
  1016. syncthing.directive('modal', function () {
  1017. return {
  1018. restrict: 'E',
  1019. templateUrl: 'modal.html',
  1020. replace: true,
  1021. transclude: true,
  1022. scope: {
  1023. title: '@',
  1024. status: '@',
  1025. icon: '@',
  1026. close: '@',
  1027. large: '@',
  1028. },
  1029. };
  1030. });