app.js 36 KB

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