app.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  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.setAPIKey = function (cfg) {
  767. cfg.APIKey = randomString(30, 32);
  768. };
  769. $scope.showURPreview = function () {
  770. $('#settings').modal('hide');
  771. $('#urPreview').modal().on('hidden.bs.modal', function () {
  772. $('#settings').modal();
  773. });
  774. };
  775. $scope.acceptUR = function () {
  776. $scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
  777. $scope.saveConfig();
  778. $('#ur').modal('hide');
  779. };
  780. $scope.declineUR = function () {
  781. $scope.config.Options.URAccepted = -1;
  782. $scope.saveConfig();
  783. $('#ur').modal('hide');
  784. };
  785. $scope.showNeed = function (repo) {
  786. $scope.neededLoaded = false;
  787. $('#needed').modal();
  788. $http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
  789. $scope.needed = data;
  790. $scope.neededLoaded = true;
  791. });
  792. };
  793. $scope.needAction = function (file) {
  794. var fDelete = 4096;
  795. var fDirectory = 16384;
  796. if ((file.Flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
  797. return 'rmdir';
  798. } else if ((file.Flags & fDelete) === fDelete) {
  799. return 'rm';
  800. } else if ((file.Flags & fDirectory) === fDirectory) {
  801. return 'touch';
  802. } else {
  803. return 'sync';
  804. }
  805. };
  806. $scope.override = function (repo) {
  807. $http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo));
  808. };
  809. $scope.about = function () {
  810. $('#about').modal('show');
  811. };
  812. $scope.showReportPreview = function () {
  813. $scope.reportPreview = true;
  814. };
  815. $scope.rescanRepo = function (repo) {
  816. $http.post(urlbase + "/scan?repo=" + encodeURIComponent(repo));
  817. };
  818. $scope.init();
  819. setInterval($scope.refresh, 10000);
  820. });
  821. function nodeCompare(a, b) {
  822. if (typeof a.Name !== 'undefined' && typeof b.Name !== 'undefined') {
  823. if (a.Name < b.Name)
  824. return -1;
  825. return a.Name > b.Name;
  826. }
  827. if (a.NodeID < b.NodeID) {
  828. return -1;
  829. }
  830. return a.NodeID > b.NodeID;
  831. }
  832. function repoCompare(a, b) {
  833. if (a.ID < b.ID) {
  834. return -1;
  835. }
  836. return a.ID > b.ID;
  837. }
  838. function repoMap(l) {
  839. var m = {};
  840. l.forEach(function (r) {
  841. m[r.ID] = r;
  842. });
  843. return m;
  844. }
  845. function repoList(m) {
  846. var l = [];
  847. for (var id in m) {
  848. l.push(m[id]);
  849. }
  850. l.sort(repoCompare);
  851. return l;
  852. }
  853. function decimals(val, num) {
  854. var digits, decs;
  855. if (val === 0) {
  856. return 0;
  857. }
  858. digits = Math.floor(Math.log(Math.abs(val)) / Math.log(10));
  859. decs = Math.max(0, num - digits);
  860. return decs;
  861. }
  862. function randomString(len, bits) {
  863. bits = bits || 36;
  864. var outStr = "",
  865. newStr;
  866. while (outStr.length < len) {
  867. newStr = Math.random().toString(bits).slice(2);
  868. outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
  869. }
  870. return outStr.toLowerCase();
  871. }
  872. function isEmptyObject(obj) {
  873. var name;
  874. for (name in obj) {
  875. return false;
  876. }
  877. return true;
  878. }
  879. syncthing.filter('natural', function () {
  880. return function (input, valid) {
  881. return input.toFixed(decimals(input, valid));
  882. };
  883. });
  884. syncthing.filter('binary', function () {
  885. return function (input) {
  886. if (input === undefined) {
  887. return '0 ';
  888. }
  889. if (input > 1024 * 1024 * 1024) {
  890. input /= 1024 * 1024 * 1024;
  891. return input.toFixed(decimals(input, 2)) + ' Gi';
  892. }
  893. if (input > 1024 * 1024) {
  894. input /= 1024 * 1024;
  895. return input.toFixed(decimals(input, 2)) + ' Mi';
  896. }
  897. if (input > 1024) {
  898. input /= 1024;
  899. return input.toFixed(decimals(input, 2)) + ' Ki';
  900. }
  901. return Math.round(input) + ' ';
  902. };
  903. });
  904. syncthing.filter('metric', function () {
  905. return function (input) {
  906. if (input === undefined) {
  907. return '0 ';
  908. }
  909. if (input > 1000 * 1000 * 1000) {
  910. input /= 1000 * 1000 * 1000;
  911. return input.toFixed(decimals(input, 2)) + ' G';
  912. }
  913. if (input > 1000 * 1000) {
  914. input /= 1000 * 1000;
  915. return input.toFixed(decimals(input, 2)) + ' M';
  916. }
  917. if (input > 1000) {
  918. input /= 1000;
  919. return input.toFixed(decimals(input, 2)) + ' k';
  920. }
  921. return Math.round(input) + ' ';
  922. };
  923. });
  924. syncthing.filter('alwaysNumber', function () {
  925. return function (input) {
  926. if (input === undefined) {
  927. return 0;
  928. }
  929. return input;
  930. };
  931. });
  932. syncthing.filter('basename', function () {
  933. return function (input) {
  934. if (input === undefined)
  935. return "";
  936. var parts = input.split(/[\/\\]/);
  937. if (!parts || parts.length < 1) {
  938. return input;
  939. }
  940. return parts[parts.length - 1];
  941. };
  942. });
  943. syncthing.directive('uniqueRepo', function () {
  944. return {
  945. require: 'ngModel',
  946. link: function (scope, elm, attrs, ctrl) {
  947. ctrl.$parsers.unshift(function (viewValue) {
  948. if (scope.editingExisting) {
  949. // we shouldn't validate
  950. ctrl.$setValidity('uniqueRepo', true);
  951. } else if (scope.repos[viewValue]) {
  952. // the repo exists already
  953. ctrl.$setValidity('uniqueRepo', false);
  954. } else {
  955. // the repo is unique
  956. ctrl.$setValidity('uniqueRepo', true);
  957. }
  958. return viewValue;
  959. });
  960. }
  961. };
  962. });
  963. syncthing.directive('validNodeid', function ($http) {
  964. return {
  965. require: 'ngModel',
  966. link: function (scope, elm, attrs, ctrl) {
  967. ctrl.$parsers.unshift(function (viewValue) {
  968. if (scope.editingExisting) {
  969. // we shouldn't validate
  970. ctrl.$setValidity('validNodeid', true);
  971. } else {
  972. $http.get(urlbase + '/nodeid?id=' + viewValue).success(function (resp) {
  973. if (resp.error) {
  974. ctrl.$setValidity('validNodeid', false);
  975. } else {
  976. ctrl.$setValidity('validNodeid', true);
  977. }
  978. });
  979. }
  980. return viewValue;
  981. });
  982. }
  983. };
  984. });
  985. syncthing.directive('modal', function () {
  986. return {
  987. restrict: 'E',
  988. templateUrl: 'modal.html',
  989. replace: true,
  990. transclude: true,
  991. scope: {
  992. title: '@',
  993. status: '@',
  994. icon: '@',
  995. close: '@',
  996. large: '@',
  997. },
  998. };
  999. });