app.js 33 KB

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