syncthingController.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. angular.module('syncthing.core')
  2. .controller('SyncthingController', function ($scope, $http, $translate, $location) {
  3. 'use strict';
  4. var prevDate = 0;
  5. var getOK = true;
  6. var navigatingAway = false;
  7. var online = false;
  8. var restarting = false;
  9. $scope.completion = {};
  10. $scope.config = {};
  11. $scope.configInSync = true;
  12. $scope.connections = {};
  13. $scope.errors = [];
  14. $scope.model = {};
  15. $scope.myID = '';
  16. $scope.devices = [];
  17. $scope.protocolChanged = false;
  18. $scope.reportData = {};
  19. $scope.reportPreview = false;
  20. $scope.folders = {};
  21. $scope.seenError = '';
  22. $scope.upgradeInfo = null;
  23. $scope.stats = {};
  24. $scope.progress = {};
  25. $http.get(urlbase + "/lang").success(function (langs) {
  26. // Find the first language in the list provided by the user's browser
  27. // that is a prefix of a language we have available. That is, "en"
  28. // sent by the browser will match "en" or "en-US", while "zh-TW" will
  29. // match only "zh-TW" and not "zh-CN".
  30. var lang, matching;
  31. for (var i = 0; i < langs.length; i++) {
  32. lang = langs[i];
  33. if (lang.length < 2) {
  34. continue;
  35. }
  36. matching = validLangs.filter(function (possibleLang) {
  37. // The langs returned by the /rest/langs call will be in lower
  38. // case. We compare to the lowercase version of the language
  39. // code we have as well.
  40. possibleLang = possibleLang.toLowerCase();
  41. if (possibleLang.length > lang.length) {
  42. return possibleLang.indexOf(lang) === 0;
  43. } else {
  44. return lang.indexOf(possibleLang) === 0;
  45. }
  46. });
  47. if (matching.length >= 1) {
  48. $translate.use(matching[0]);
  49. return;
  50. }
  51. }
  52. // Fallback if nothing matched
  53. $translate.use("en");
  54. });
  55. $(window).bind('beforeunload', function () {
  56. navigatingAway = true;
  57. });
  58. $scope.$on("$locationChangeSuccess", function () {
  59. var lang = $location.search().lang;
  60. if (lang) {
  61. $translate.use(lang);
  62. }
  63. });
  64. $scope.needActions = {
  65. 'rm': 'Del',
  66. 'rmdir': 'Del (dir)',
  67. 'sync': 'Sync',
  68. 'touch': 'Update'
  69. };
  70. $scope.needIcons = {
  71. 'rm': 'remove',
  72. 'rmdir': 'remove',
  73. 'sync': 'download',
  74. 'touch': 'asterisk'
  75. };
  76. $scope.$on('UIOnline', function (event, arg) {
  77. if (online && !restarting) {
  78. return;
  79. }
  80. console.log('UIOnline');
  81. $scope.init();
  82. online = true;
  83. restarting = false;
  84. $('#networkError').modal('hide');
  85. $('#restarting').modal('hide');
  86. $('#shutdown').modal('hide');
  87. });
  88. $scope.$on('UIOffline', function (event, arg) {
  89. if (navigatingAway || !online) {
  90. return;
  91. }
  92. console.log('UIOffline');
  93. online = false;
  94. if (!restarting) {
  95. $('#networkError').modal();
  96. }
  97. });
  98. $scope.$on('StateChanged', function (event, arg) {
  99. var data = arg.data;
  100. if ($scope.model[data.folder]) {
  101. $scope.model[data.folder].state = data.to;
  102. }
  103. });
  104. $scope.$on('LocalIndexUpdated', function (event, arg) {
  105. var data = arg.data;
  106. refreshFolder(data.folder);
  107. // Update completion status for all devices that we share this folder with.
  108. $scope.folders[data.folder].Devices.forEach(function (deviceCfg) {
  109. refreshCompletion(deviceCfg.DeviceID, data.folder);
  110. });
  111. });
  112. $scope.$on('RemoteIndexUpdated', function (event, arg) {
  113. var data = arg.data;
  114. refreshFolder(data.folder);
  115. refreshCompletion(data.device, data.folder);
  116. });
  117. $scope.$on('DeviceDisconnected', function (event, arg) {
  118. delete $scope.connections[arg.data.id];
  119. refreshDeviceStats();
  120. });
  121. $scope.$on('DeviceConnected', function (event, arg) {
  122. if (!$scope.connections[arg.data.id]) {
  123. $scope.connections[arg.data.id] = {
  124. inbps: 0,
  125. outbps: 0,
  126. InBytesTotal: 0,
  127. OutBytesTotal: 0,
  128. Address: arg.data.addr
  129. };
  130. $scope.completion[arg.data.id] = {
  131. _total: 100
  132. };
  133. }
  134. });
  135. $scope.$on('ConfigLoaded', function (event) {
  136. if ($scope.config.Options.URAccepted === 0) {
  137. // If usage reporting has been neither accepted nor declined,
  138. // we want to ask the user to make a choice. But we don't want
  139. // to bug them during initial setup, so we set a cookie with
  140. // the time of the first visit. When that cookie is present
  141. // and the time is more than four hours ago, we ask the
  142. // question.
  143. var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  144. if (!firstVisit) {
  145. document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
  146. } else {
  147. if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
  148. $('#ur').modal();
  149. }
  150. }
  151. }
  152. });
  153. $scope.$on('ConfigSaved', function (event, arg) {
  154. updateLocalConfig(arg.data);
  155. $http.get(urlbase + '/config/sync').success(function (data) {
  156. $scope.configInSync = data.configInSync;
  157. });
  158. });
  159. $scope.$on('DownloadProgress', function (event, arg) {
  160. var stats = arg.data;
  161. var progress = {};
  162. for(var folder in stats){
  163. refreshFolder(folder);
  164. progress[folder] = {};
  165. for(var file in stats[folder]){
  166. var s = stats[folder][file];
  167. var reused = Math.floor(100 * s.Reused / s.Total);
  168. var copiedFromOrigin = Math.floor(100 * s.CopiedFromOrigin / s.Total);
  169. var copiedFromElsewhere = Math.floor(100 * s.CopiedFromElsewhere / s.Total);
  170. var pulled = Math.floor(100 * s.Pulled / s.Total);
  171. var pulling = Math.floor(100 * s.Pulling / s.Total);
  172. // We can do the following, because if s.Pulling > 0, than reused + copied + pulled < 100 because off rounding them down.
  173. // We do this to show which files are currently being pulled
  174. if (s.Pulling && pulling == 0) {
  175. pulling = 1;
  176. }
  177. progress[folder][file] = {
  178. Reused: reused,
  179. CopiedFromOrigin: copiedFromOrigin,
  180. CopiedFromElsewhere: copiedFromElsewhere,
  181. Pulled: pulled,
  182. Pulling: pulling,
  183. BytesTotal: s.BytesTotal,
  184. BytesDone: s.BytesDone,
  185. };
  186. }
  187. }
  188. for(var folder in $scope.progress){
  189. if (!(folder in progress)) {
  190. refreshFolder(folder);
  191. if ($scope.neededFolder == folder) {
  192. refreshNeed(folder);
  193. }
  194. } else if ($scope.neededFolder == folder) {
  195. for(file in $scope.progress[folder]){
  196. if (!(file in progress[folder])) {
  197. refreshNeed(folder);
  198. break;
  199. }
  200. }
  201. }
  202. }
  203. $scope.progress = progress;
  204. console.log("DownloadProgress", $scope.progress);
  205. });
  206. var debouncedFuncs = {};
  207. function refreshFolder(folder) {
  208. var key = "refreshFolder" + folder;
  209. if (!debouncedFuncs[key]) {
  210. debouncedFuncs[key] = debounce(function () {
  211. $http.get(urlbase + '/model?folder=' + encodeURIComponent(folder)).success(function (data) {
  212. $scope.model[folder] = data;
  213. console.log("refreshFolder", folder, data);
  214. });
  215. }, 1000, true);
  216. }
  217. debouncedFuncs[key]();
  218. }
  219. function updateLocalConfig(config) {
  220. var hasConfig = !isEmptyObject($scope.config);
  221. $scope.config = config;
  222. $scope.config.Options.ListenAddressStr = $scope.config.Options.ListenAddress.join(', ');
  223. $scope.config.Options.GlobalAnnServersStr = $scope.config.Options.GlobalAnnServers.join(', ');
  224. $scope.devices = $scope.config.Devices;
  225. $scope.devices.forEach(function (deviceCfg) {
  226. $scope.completion[deviceCfg.DeviceID] = {
  227. _total: 100
  228. };
  229. });
  230. $scope.devices.sort(deviceCompare);
  231. $scope.folders = folderMap($scope.config.Folders);
  232. Object.keys($scope.folders).forEach(function (folder) {
  233. refreshFolder(folder);
  234. $scope.folders[folder].Devices.forEach(function (deviceCfg) {
  235. refreshCompletion(deviceCfg.DeviceID, folder);
  236. });
  237. });
  238. if (!hasConfig) {
  239. $scope.$emit('ConfigLoaded');
  240. }
  241. }
  242. function refreshSystem() {
  243. $http.get(urlbase + '/system').success(function (data) {
  244. $scope.myID = data.myID;
  245. $scope.system = data;
  246. $scope.announceServersTotal = Object.keys(data.extAnnounceOK).length;
  247. var failed = [];
  248. for (var server in data.extAnnounceOK) {
  249. if (!data.extAnnounceOK[server]) {
  250. failed.push(server);
  251. }
  252. }
  253. $scope.announceServersFailed = failed;
  254. console.log("refreshSystem", data);
  255. });
  256. }
  257. function refreshCompletion(device, folder) {
  258. if (device === $scope.myID) {
  259. return;
  260. }
  261. var key = "refreshCompletion" + device + folder;
  262. if (!debouncedFuncs[key]) {
  263. debouncedFuncs[key] = debounce(function () {
  264. $http.get(urlbase + '/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) {
  265. if (!$scope.completion[device]) {
  266. $scope.completion[device] = {};
  267. }
  268. $scope.completion[device][folder] = data.completion;
  269. var tot = 0,
  270. cnt = 0;
  271. for (var cmp in $scope.completion[device]) {
  272. if (cmp === "_total") {
  273. continue;
  274. }
  275. tot += $scope.completion[device][cmp];
  276. cnt += 1;
  277. }
  278. $scope.completion[device]._total = tot / cnt;
  279. console.log("refreshCompletion", device, folder, $scope.completion[device]);
  280. });
  281. }, 1000, true);
  282. }
  283. debouncedFuncs[key]();
  284. }
  285. function refreshConnectionStats() {
  286. $http.get(urlbase + '/connections').success(function (data) {
  287. var now = Date.now(),
  288. td = (now - prevDate) / 1000,
  289. id;
  290. prevDate = now;
  291. for (id in data) {
  292. if (!data.hasOwnProperty(id)) {
  293. continue;
  294. }
  295. try {
  296. data[id].inbps = Math.max(0, (data[id].InBytesTotal - $scope.connections[id].InBytesTotal) / td);
  297. data[id].outbps = Math.max(0, (data[id].OutBytesTotal - $scope.connections[id].OutBytesTotal) / td);
  298. } catch (e) {
  299. data[id].inbps = 0;
  300. data[id].outbps = 0;
  301. }
  302. }
  303. $scope.connections = data;
  304. console.log("refreshConnections", data);
  305. });
  306. }
  307. function refreshErrors() {
  308. $http.get(urlbase + '/errors').success(function (data) {
  309. $scope.errors = data.errors;
  310. console.log("refreshErrors", data);
  311. });
  312. }
  313. function refreshConfig() {
  314. $http.get(urlbase + '/config').success(function (data) {
  315. updateLocalConfig(data);
  316. console.log("refreshConfig", data);
  317. });
  318. $http.get(urlbase + '/config/sync').success(function (data) {
  319. $scope.configInSync = data.configInSync;
  320. });
  321. }
  322. function refreshNeed(folder) {
  323. $http.get(urlbase + "/need?folder=" + encodeURIComponent(folder)).success(function (data) {
  324. if ($scope.neededFolder == folder) {
  325. console.log("refreshNeed", folder, data);
  326. $scope.needed = data;
  327. }
  328. });
  329. }
  330. var refreshDeviceStats = debounce(function () {
  331. $http.get(urlbase + "/stats/device").success(function (data) {
  332. $scope.stats = data;
  333. for (var device in $scope.stats) {
  334. $scope.stats[device].LastSeen = new Date($scope.stats[device].LastSeen);
  335. $scope.stats[device].LastSeenDays = (new Date() - $scope.stats[device].LastSeen) / 1000 / 86400;
  336. }
  337. console.log("refreshDeviceStats", data);
  338. });
  339. }, 500);
  340. $scope.init = function () {
  341. refreshSystem();
  342. refreshConfig();
  343. refreshConnectionStats();
  344. refreshDeviceStats();
  345. $http.get(urlbase + '/version').success(function (data) {
  346. $scope.version = data.version;
  347. });
  348. $http.get(urlbase + '/report').success(function (data) {
  349. $scope.reportData = data;
  350. });
  351. $http.get(urlbase + '/upgrade').success(function (data) {
  352. $scope.upgradeInfo = data;
  353. }).error(function () {
  354. $scope.upgradeInfo = null;
  355. });
  356. };
  357. $scope.refresh = function () {
  358. refreshSystem();
  359. refreshConnectionStats();
  360. refreshErrors();
  361. };
  362. $scope.folderStatus = function (folderCfg) {
  363. if (typeof $scope.model[folderCfg.ID] === 'undefined') {
  364. return 'unknown';
  365. }
  366. if (folderCfg.Devices.length <= 1) {
  367. return 'unshared';
  368. }
  369. if ($scope.model[folderCfg.ID].invalid !== '') {
  370. return 'stopped';
  371. }
  372. return '' + $scope.model[folderCfg.ID].state;
  373. };
  374. $scope.folderClass = function (folderCfg) {
  375. if (typeof $scope.model[folderCfg.ID] === 'undefined') {
  376. // Unknown
  377. return 'info';
  378. }
  379. if (folderCfg.Devices.length <= 1) {
  380. // Unshared
  381. return 'warning';
  382. }
  383. if ($scope.model[folderCfg.ID].invalid !== '') {
  384. // Errored
  385. return 'danger';
  386. }
  387. var state = '' + $scope.model[folderCfg.ID].state;
  388. if (state == 'idle') {
  389. return 'success';
  390. }
  391. if (state == 'syncing') {
  392. return 'primary';
  393. }
  394. if (state == 'scanning') {
  395. return 'primary';
  396. }
  397. return 'info';
  398. };
  399. $scope.syncPercentage = function (folder) {
  400. if (typeof $scope.model[folder] === 'undefined') {
  401. return 100;
  402. }
  403. if ($scope.model[folder].globalBytes === 0) {
  404. return 100;
  405. }
  406. var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes;
  407. return Math.floor(pct);
  408. };
  409. $scope.deviceIcon = function (deviceCfg) {
  410. if ($scope.connections[deviceCfg.DeviceID]) {
  411. if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) {
  412. return 'ok';
  413. } else {
  414. return 'refresh';
  415. }
  416. }
  417. return 'minus';
  418. };
  419. $scope.deviceStatus = function (deviceCfg) {
  420. if ($scope.deviceFolders(deviceCfg).length === 0) {
  421. return 'unused';
  422. }
  423. if ($scope.connections[deviceCfg.DeviceID]) {
  424. if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) {
  425. return 'insync';
  426. } else {
  427. return 'syncing';
  428. }
  429. }
  430. // Disconnected
  431. return 'disconnected';
  432. };
  433. $scope.deviceClass = function (deviceCfg) {
  434. if ($scope.deviceFolders(deviceCfg).length === 0) {
  435. // Unused
  436. return 'warning';
  437. }
  438. if ($scope.connections[deviceCfg.DeviceID]) {
  439. if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) {
  440. return 'success';
  441. } else {
  442. return 'primary';
  443. }
  444. }
  445. // Disconnected
  446. return 'info';
  447. };
  448. $scope.deviceAddr = function (deviceCfg) {
  449. var conn = $scope.connections[deviceCfg.DeviceID];
  450. if (conn) {
  451. return conn.Address;
  452. }
  453. return '?';
  454. };
  455. $scope.deviceCompletion = function (deviceCfg) {
  456. var conn = $scope.connections[deviceCfg.DeviceID];
  457. if (conn) {
  458. return conn.Completion + '%';
  459. }
  460. return '';
  461. };
  462. $scope.findDevice = function (deviceID) {
  463. var matches = $scope.devices.filter(function (n) {
  464. return n.DeviceID == deviceID;
  465. });
  466. if (matches.length != 1) {
  467. return undefined;
  468. }
  469. return matches[0];
  470. };
  471. $scope.deviceName = function (deviceCfg) {
  472. if (typeof deviceCfg === 'undefined') {
  473. return "";
  474. }
  475. if (deviceCfg.Name) {
  476. return deviceCfg.Name;
  477. }
  478. return deviceCfg.DeviceID.substr(0, 6);
  479. };
  480. $scope.thisDeviceName = function () {
  481. var device = $scope.thisDevice();
  482. if (typeof device === 'undefined') {
  483. return "(unknown device)";
  484. }
  485. if (device.Name) {
  486. return device.Name;
  487. }
  488. return device.DeviceID.substr(0, 6);
  489. };
  490. $scope.editSettings = function () {
  491. // Make a working copy
  492. $scope.tmpOptions = angular.copy($scope.config.Options);
  493. $scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
  494. $scope.tmpOptions.DeviceName = $scope.thisDevice().Name;
  495. $scope.tmpOptions.AutoUpgradeEnabled = ($scope.tmpOptions.AutoUpgradeIntervalH > 0);
  496. $scope.tmpGUI = angular.copy($scope.config.GUI);
  497. $('#settings').modal();
  498. };
  499. $scope.saveConfig = function () {
  500. var cfg = JSON.stringify($scope.config);
  501. var opts = {
  502. headers: {
  503. 'Content-Type': 'application/json'
  504. }
  505. };
  506. $http.post(urlbase + '/config', cfg, opts).success(function () {
  507. $http.get(urlbase + '/config/sync').success(function (data) {
  508. $scope.configInSync = data.configInSync;
  509. });
  510. });
  511. };
  512. $scope.saveSettings = function () {
  513. // Make sure something changed
  514. var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) || !angular.equals($scope.config.GUI, $scope.tmpGUI);
  515. if (changed) {
  516. // Check if usage reporting has been enabled or disabled
  517. if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
  518. $scope.tmpOptions.URAccepted = 1000;
  519. } else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0) {
  520. $scope.tmpOptions.URAccepted = -1;
  521. }
  522. // Check if auto-upgrade has been enabled or disabled
  523. if ($scope.tmpOptions.AutoUpgradeEnabled) {
  524. $scope.tmpOptions.AutoUpgradeIntervalH = $scope.tmpOptions.AutoUpgradeIntervalH || 12;
  525. } else {
  526. $scope.tmpOptions.AutoUpgradeIntervalH = 0;
  527. }
  528. // Check if protocol will need to be changed on restart
  529. if ($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS) {
  530. $scope.protocolChanged = true;
  531. }
  532. // Apply new settings locally
  533. $scope.thisDevice().Name = $scope.tmpOptions.DeviceName;
  534. $scope.config.Options = angular.copy($scope.tmpOptions);
  535. $scope.config.GUI = angular.copy($scope.tmpGUI);
  536. ['ListenAddress', 'GlobalAnnServers'].forEach(function (key) {
  537. $scope.config.Options[key] = $scope.config.Options[key + "Str"].split(/[ ,]+/).map(function (x) {
  538. return x.trim();
  539. });
  540. });
  541. $scope.saveConfig();
  542. }
  543. $('#settings').modal("hide");
  544. };
  545. $scope.restart = function () {
  546. restarting = true;
  547. $('#restarting').modal();
  548. $http.post(urlbase + '/restart');
  549. $scope.configInSync = true;
  550. // Switch webpage protocol if needed
  551. if ($scope.protocolChanged) {
  552. var protocol = 'http';
  553. if ($scope.config.GUI.UseTLS) {
  554. protocol = 'https';
  555. }
  556. setTimeout(function () {
  557. window.location.protocol = protocol;
  558. }, 2500);
  559. $scope.protocolChanged = false;
  560. }
  561. };
  562. $scope.upgrade = function () {
  563. restarting = true;
  564. $('#upgrading').modal();
  565. $http.post(urlbase + '/upgrade').success(function () {
  566. $('#restarting').modal();
  567. $('#upgrading').modal('hide');
  568. }).error(function () {
  569. $('#upgrading').modal('hide');
  570. });
  571. };
  572. $scope.shutdown = function () {
  573. restarting = true;
  574. $http.post(urlbase + '/shutdown').success(function () {
  575. $('#shutdown').modal();
  576. });
  577. $scope.configInSync = true;
  578. };
  579. $scope.editDevice = function (deviceCfg) {
  580. $scope.currentDevice = $.extend({}, deviceCfg);
  581. $scope.editingExisting = true;
  582. $scope.editingSelf = (deviceCfg.DeviceID == $scope.myID);
  583. $scope.currentDevice.AddressesStr = deviceCfg.Addresses.join(', ');
  584. $scope.deviceEditor.$setPristine();
  585. $('#editDevice').modal();
  586. };
  587. $scope.idDevice = function () {
  588. $('#idqr').modal('show');
  589. };
  590. $scope.addDevice = function () {
  591. $http.get(urlbase + '/discovery')
  592. .success(function (registry) {
  593. $scope.discovery = registry;
  594. })
  595. .then(function () {
  596. $scope.currentDevice = {
  597. AddressesStr: 'dynamic',
  598. Compression: true,
  599. Introducer: false
  600. };
  601. $scope.editingExisting = false;
  602. $scope.editingSelf = false;
  603. $scope.deviceEditor.$setPristine();
  604. $('#editDevice').modal();
  605. });
  606. };
  607. $scope.deleteDevice = function () {
  608. $('#editDevice').modal('hide');
  609. if (!$scope.editingExisting) {
  610. return;
  611. }
  612. $scope.devices = $scope.devices.filter(function (n) {
  613. return n.DeviceID !== $scope.currentDevice.DeviceID;
  614. });
  615. $scope.config.Devices = $scope.devices;
  616. for (var id in $scope.folders) {
  617. $scope.folders[id].Devices = $scope.folders[id].Devices.filter(function (n) {
  618. return n.DeviceID !== $scope.currentDevice.DeviceID;
  619. });
  620. }
  621. $scope.saveConfig();
  622. };
  623. $scope.saveDevice = function () {
  624. var deviceCfg, done, i;
  625. $('#editDevice').modal('hide');
  626. deviceCfg = $scope.currentDevice;
  627. deviceCfg.Addresses = deviceCfg.AddressesStr.split(',').map(function (x) {
  628. return x.trim();
  629. });
  630. done = false;
  631. for (i = 0; i < $scope.devices.length; i++) {
  632. if ($scope.devices[i].DeviceID === deviceCfg.DeviceID) {
  633. $scope.devices[i] = deviceCfg;
  634. done = true;
  635. break;
  636. }
  637. }
  638. if (!done) {
  639. $scope.devices.push(deviceCfg);
  640. }
  641. $scope.devices.sort(deviceCompare);
  642. $scope.config.Devices = $scope.devices;
  643. $scope.saveConfig();
  644. };
  645. $scope.otherDevices = function () {
  646. return $scope.devices.filter(function (n) {
  647. return n.DeviceID !== $scope.myID;
  648. });
  649. };
  650. $scope.thisDevice = function () {
  651. var i, n;
  652. for (i = 0; i < $scope.devices.length; i++) {
  653. n = $scope.devices[i];
  654. if (n.DeviceID === $scope.myID) {
  655. return n;
  656. }
  657. }
  658. };
  659. $scope.allDevices = function () {
  660. var devices = $scope.otherDevices();
  661. devices.push($scope.thisDevice());
  662. return devices;
  663. };
  664. $scope.errorList = function () {
  665. return $scope.errors.filter(function (e) {
  666. return e.Time > $scope.seenError;
  667. });
  668. };
  669. $scope.clearErrors = function () {
  670. $scope.seenError = $scope.errors[$scope.errors.length - 1].Time;
  671. $http.post(urlbase + '/error/clear');
  672. };
  673. $scope.friendlyDevices = function (str) {
  674. for (var i = 0; i < $scope.devices.length; i++) {
  675. var cfg = $scope.devices[i];
  676. str = str.replace(cfg.DeviceID, $scope.deviceName(cfg));
  677. }
  678. return str;
  679. };
  680. $scope.folderList = function () {
  681. return folderList($scope.folders);
  682. };
  683. $scope.directoryList = [];
  684. $scope.$watch('currentFolder.Path', function (newvalue) {
  685. $http.get(urlbase + '/autocomplete/directory', {
  686. params: { current: newvalue }
  687. }).success(function (data) {
  688. $scope.directoryList = data;
  689. });
  690. });
  691. $scope.editFolder = function (deviceCfg) {
  692. $scope.currentFolder = angular.copy(deviceCfg);
  693. $scope.currentFolder.selectedDevices = {};
  694. $scope.currentFolder.Devices.forEach(function (n) {
  695. $scope.currentFolder.selectedDevices[n.DeviceID] = true;
  696. });
  697. if ($scope.currentFolder.Versioning && $scope.currentFolder.Versioning.Type === "simple") {
  698. $scope.currentFolder.simpleFileVersioning = true;
  699. $scope.currentFolder.FileVersioningSelector = "simple";
  700. $scope.currentFolder.simpleKeep = +$scope.currentFolder.Versioning.Params.keep;
  701. } else if ($scope.currentFolder.Versioning && $scope.currentFolder.Versioning.Type === "staggered") {
  702. $scope.currentFolder.staggeredFileVersioning = true;
  703. $scope.currentFolder.FileVersioningSelector = "staggered";
  704. $scope.currentFolder.staggeredMaxAge = Math.floor(+$scope.currentFolder.Versioning.Params.maxAge / 86400);
  705. $scope.currentFolder.staggeredCleanInterval = +$scope.currentFolder.Versioning.Params.cleanInterval;
  706. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.Versioning.Params.versionsPath;
  707. } else {
  708. $scope.currentFolder.FileVersioningSelector = "none";
  709. }
  710. $scope.currentFolder.simpleKeep = $scope.currentFolder.simpleKeep || 5;
  711. $scope.currentFolder.staggeredCleanInterval = $scope.currentFolder.staggeredCleanInterval || 3600;
  712. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.staggeredVersionsPath || "";
  713. // staggeredMaxAge can validly be zero, which we should not replace
  714. // with the default value of 365. So only set the default if it's
  715. // actually undefined.
  716. if (typeof $scope.currentFolder.staggeredMaxAge === 'undefined') {
  717. $scope.currentFolder.staggeredMaxAge = 365;
  718. }
  719. $scope.editingExisting = true;
  720. $scope.folderEditor.$setPristine();
  721. $('#editFolder').modal();
  722. };
  723. $scope.addFolder = function () {
  724. $scope.currentFolder = {
  725. selectedDevices: {}
  726. };
  727. $scope.currentFolder.RescanIntervalS = 60;
  728. $scope.currentFolder.FileVersioningSelector = "none";
  729. $scope.currentFolder.simpleKeep = 5;
  730. $scope.currentFolder.staggeredMaxAge = 365;
  731. $scope.currentFolder.staggeredCleanInterval = 3600;
  732. $scope.currentFolder.staggeredVersionsPath = "";
  733. $scope.editingExisting = false;
  734. $scope.folderEditor.$setPristine();
  735. $('#editFolder').modal();
  736. };
  737. $scope.saveFolder = function () {
  738. var folderCfg, done, i;
  739. $('#editFolder').modal('hide');
  740. folderCfg = $scope.currentFolder;
  741. folderCfg.Devices = [];
  742. folderCfg.selectedDevices[$scope.myID] = true;
  743. for (var deviceID in folderCfg.selectedDevices) {
  744. if (folderCfg.selectedDevices[deviceID] === true) {
  745. folderCfg.Devices.push({
  746. DeviceID: deviceID
  747. });
  748. }
  749. }
  750. delete folderCfg.selectedDevices;
  751. if (folderCfg.FileVersioningSelector === "simple") {
  752. folderCfg.Versioning = {
  753. 'Type': 'simple',
  754. 'Params': {
  755. 'keep': '' + folderCfg.simpleKeep
  756. }
  757. };
  758. delete folderCfg.simpleFileVersioning;
  759. delete folderCfg.simpleKeep;
  760. } else if (folderCfg.FileVersioningSelector === "staggered") {
  761. folderCfg.Versioning = {
  762. 'Type': 'staggered',
  763. 'Params': {
  764. 'maxAge': '' + (folderCfg.staggeredMaxAge * 86400),
  765. 'cleanInterval': '' + folderCfg.staggeredCleanInterval,
  766. 'versionsPath': '' + folderCfg.staggeredVersionsPath
  767. }
  768. };
  769. delete folderCfg.staggeredFileVersioning;
  770. delete folderCfg.staggeredMaxAge;
  771. delete folderCfg.staggeredCleanInterval;
  772. delete folderCfg.staggeredVersionsPath;
  773. } else {
  774. delete folderCfg.Versioning;
  775. }
  776. $scope.folders[folderCfg.ID] = folderCfg;
  777. $scope.config.Folders = folderList($scope.folders);
  778. $scope.saveConfig();
  779. };
  780. $scope.deviceFolders = function (deviceCfg) {
  781. var folders = [];
  782. for (var folderID in $scope.folders) {
  783. var devices = $scope.folders[folderID].Devices
  784. for (var i = 0; i < devices.length; i++) {
  785. if (devices[i].DeviceID == deviceCfg.DeviceID) {
  786. folders.push(folderID)
  787. break
  788. }
  789. }
  790. };
  791. folders.sort();
  792. return folders;
  793. };
  794. $scope.deleteFolder = function () {
  795. $('#editFolder').modal('hide');
  796. if (!$scope.editingExisting) {
  797. return;
  798. }
  799. delete $scope.folders[$scope.currentFolder.ID];
  800. $scope.config.Folders = folderList($scope.folders);
  801. $scope.saveConfig();
  802. };
  803. $scope.editIgnores = function () {
  804. if (!$scope.editingExisting) {
  805. return;
  806. }
  807. $('#editIgnoresButton').attr('disabled', 'disabled');
  808. $http.get(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.ID))
  809. .success(function (data) {
  810. data.ignore = data.ignore || [];
  811. $('#editFolder').modal('hide');
  812. var textArea = $('#editIgnores textarea');
  813. textArea.val(data.ignore.join('\n'));
  814. $('#editIgnores').modal()
  815. .on('hidden.bs.modal', function () {
  816. $('#editFolder').modal();
  817. })
  818. .on('shown.bs.modal', function () {
  819. textArea.focus();
  820. });
  821. })
  822. .then(function () {
  823. $('#editIgnoresButton').removeAttr('disabled');
  824. });
  825. };
  826. $scope.saveIgnores = function () {
  827. if (!$scope.editingExisting) {
  828. return;
  829. }
  830. $http.post(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.ID), {
  831. ignore: $('#editIgnores textarea').val().split('\n')
  832. });
  833. };
  834. $scope.setAPIKey = function (cfg) {
  835. cfg.APIKey = randomString(30, 32);
  836. };
  837. $scope.showURPreview = function () {
  838. $('#settings').modal('hide');
  839. $('#urPreview').modal().on('hidden.bs.modal', function () {
  840. $('#settings').modal();
  841. });
  842. };
  843. $scope.acceptUR = function () {
  844. $scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
  845. $scope.saveConfig();
  846. $('#ur').modal('hide');
  847. };
  848. $scope.declineUR = function () {
  849. $scope.config.Options.URAccepted = -1;
  850. $scope.saveConfig();
  851. $('#ur').modal('hide');
  852. };
  853. $scope.showNeed = function (folder) {
  854. $scope.neededFolder = folder;
  855. refreshNeed(folder);
  856. $('#needed').modal().on('hidden.bs.modal', function(){
  857. $scope.neededFolder = undefined;
  858. $scope.needed = undefined;
  859. });
  860. };
  861. $scope.needAction = function (file) {
  862. var fDelete = 4096;
  863. var fDirectory = 16384;
  864. if ((file.Flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
  865. return 'rmdir';
  866. } else if ((file.Flags & fDelete) === fDelete) {
  867. return 'rm';
  868. } else if ((file.Flags & fDirectory) === fDirectory) {
  869. return 'touch';
  870. } else {
  871. return 'sync';
  872. }
  873. };
  874. $scope.override = function (folder) {
  875. $http.post(urlbase + "/model/override?folder=" + encodeURIComponent(folder));
  876. };
  877. $scope.about = function () {
  878. $('#about').modal('show');
  879. };
  880. $scope.showReportPreview = function () {
  881. $scope.reportPreview = true;
  882. };
  883. $scope.rescanFolder = function (folder) {
  884. $http.post(urlbase + "/scan?folder=" + encodeURIComponent(folder));
  885. };
  886. $scope.init();
  887. setInterval($scope.refresh, 10000);
  888. });