app.js 30 KB

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