syncthingController.js 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626
  1. angular.module('syncthing.core')
  2. .config(function($locationProvider) {
  3. $locationProvider.html5Mode({enabled: true, requireBase: false}).hashPrefix('!');
  4. })
  5. .controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter) {
  6. 'use strict';
  7. // private/helper definitions
  8. var prevDate = 0;
  9. var navigatingAway = false;
  10. var online = false;
  11. var restarting = false;
  12. function initController() {
  13. LocaleService.autoConfigLocale();
  14. setInterval($scope.refresh, 10000);
  15. Events.start();
  16. }
  17. // public/scope definitions
  18. $scope.completion = {};
  19. $scope.config = {};
  20. $scope.configInSync = true;
  21. $scope.connections = {};
  22. $scope.errors = [];
  23. $scope.model = {};
  24. $scope.myID = '';
  25. $scope.devices = [];
  26. $scope.deviceRejections = {};
  27. $scope.folderRejections = {};
  28. $scope.protocolChanged = false;
  29. $scope.reportData = {};
  30. $scope.reportPreview = false;
  31. $scope.folders = {};
  32. $scope.seenError = '';
  33. $scope.upgradeInfo = null;
  34. $scope.deviceStats = {};
  35. $scope.folderStats = {};
  36. $scope.progress = {};
  37. $scope.version = {};
  38. $scope.needed = [];
  39. $scope.neededTotal = 0;
  40. $scope.neededCurrentPage = 1;
  41. $scope.neededPageSize = 10;
  42. $scope.failed = {};
  43. $scope.failedCurrentPage = 1;
  44. $scope.failedCurrentFolder = undefined;
  45. $scope.failedPageSize = 10;
  46. $scope.scanProgress = {};
  47. $scope.themes = [];
  48. $scope.localStateTotal = {
  49. bytes: 0,
  50. files: 0
  51. };
  52. $(window).bind('beforeunload', function () {
  53. navigatingAway = true;
  54. });
  55. $scope.$on("$locationChangeSuccess", function () {
  56. LocaleService.useLocale($location.search().lang);
  57. });
  58. $scope.needActions = {
  59. 'rm': 'Del',
  60. 'rmdir': 'Del (dir)',
  61. 'sync': 'Sync',
  62. 'touch': 'Update'
  63. };
  64. $scope.needIcons = {
  65. 'rm': 'trash-o',
  66. 'rmdir': 'trash-o',
  67. 'sync': 'arrow-circle-o-down',
  68. 'touch': 'asterisk'
  69. };
  70. $scope.$on(Events.ONLINE, function () {
  71. if (online && !restarting) {
  72. return;
  73. }
  74. console.log('UIOnline');
  75. refreshSystem();
  76. refreshConfig();
  77. refreshConnectionStats();
  78. refreshDeviceStats();
  79. refreshFolderStats();
  80. refreshThemes();
  81. $http.get(urlbase + '/system/version').success(function (data) {
  82. if ($scope.version.version && $scope.version.version !== data.version) {
  83. // We already have a version response, but it differs from
  84. // the new one. Reload the full GUI in case it's changed.
  85. document.location.reload(true);
  86. }
  87. $scope.version = data;
  88. $scope.version.isDevelopmentVersion = data.version.indexOf('-')>0;
  89. }).error($scope.emitHTTPError);
  90. $http.get(urlbase + '/svc/report').success(function (data) {
  91. $scope.reportData = data;
  92. }).error($scope.emitHTTPError);
  93. $http.get(urlbase + '/system/upgrade').success(function (data) {
  94. $scope.upgradeInfo = data;
  95. }).error(function () {
  96. $scope.upgradeInfo = null;
  97. });
  98. online = true;
  99. restarting = false;
  100. $('#networkError').modal('hide');
  101. $('#restarting').modal('hide');
  102. $('#shutdown').modal('hide');
  103. });
  104. $scope.$on(Events.OFFLINE, function () {
  105. if (navigatingAway || !online) {
  106. return;
  107. }
  108. console.log('UIOffline');
  109. online = false;
  110. if (!restarting) {
  111. $('#networkError').modal();
  112. }
  113. });
  114. $scope.$on('HTTPError', function (event, arg) {
  115. // Emitted when a HTTP call fails. We use the status code to try
  116. // to figure out what's wrong.
  117. if (navigatingAway || !online) {
  118. return;
  119. }
  120. console.log('HTTPError', arg);
  121. online = false;
  122. if (!restarting) {
  123. if (arg.status === 0) {
  124. // A network error, not an HTTP error
  125. $scope.$emit(Events.OFFLINE);
  126. } else if (arg.status >= 400 && arg.status <= 599) {
  127. // A genuine HTTP error
  128. $('#networkError').modal('hide');
  129. $('#restarting').modal('hide');
  130. $('#shutdown').modal('hide');
  131. $('#httpError').modal();
  132. }
  133. }
  134. });
  135. $scope.$on(Events.STATE_CHANGED, function (event, arg) {
  136. var data = arg.data;
  137. if ($scope.model[data.folder]) {
  138. $scope.model[data.folder].state = data.to;
  139. $scope.model[data.folder].error = data.error;
  140. // If a folder has started syncing, then any old list of
  141. // errors is obsolete. We may get a new list of errors very
  142. // shortly though.
  143. if (data.to === 'syncing') {
  144. $scope.failed[data.folder] = [];
  145. }
  146. // If a folder has started scanning, then any scan progress is
  147. // also obsolete.
  148. if (data.to === 'scanning') {
  149. delete $scope.scanProgress[data.folder];
  150. }
  151. // If a folder finished scanning, then refresh folder stats
  152. // to update last scan time.
  153. if(data.from === 'scanning' && data.to === 'idle') {
  154. refreshFolderStats();
  155. }
  156. }
  157. });
  158. $scope.$on(Events.LOCAL_INDEX_UPDATED, function (event, arg) {
  159. refreshFolderStats();
  160. });
  161. $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
  162. $scope.connections[arg.data.id].connected = false;
  163. refreshDeviceStats();
  164. });
  165. $scope.$on(Events.DEVICE_CONNECTED, function (event, arg) {
  166. if (!$scope.connections[arg.data.id]) {
  167. $scope.connections[arg.data.id] = {
  168. inbps: 0,
  169. outbps: 0,
  170. inBytesTotal: 0,
  171. outBytesTotal: 0,
  172. type: arg.data.type,
  173. address: arg.data.addr
  174. };
  175. $scope.completion[arg.data.id] = {
  176. _total: 100
  177. };
  178. }
  179. });
  180. $scope.$on('ConfigLoaded', function () {
  181. if ($scope.config.options.urAccepted === 0) {
  182. // If usage reporting has been neither accepted nor declined,
  183. // we want to ask the user to make a choice. But we don't want
  184. // to bug them during initial setup, so we set a cookie with
  185. // the time of the first visit. When that cookie is present
  186. // and the time is more than four hours ago, we ask the
  187. // question.
  188. var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  189. if (!firstVisit) {
  190. document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
  191. } else {
  192. if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
  193. $('#ur').modal();
  194. }
  195. }
  196. }
  197. });
  198. $scope.$on(Events.DEVICE_REJECTED, function (event, arg) {
  199. $scope.deviceRejections[arg.data.device] = arg;
  200. });
  201. $scope.$on(Events.DEVICE_PAUSED, function (event, arg) {
  202. $scope.connections[arg.data.device].paused = true;
  203. });
  204. $scope.$on(Events.DEVICE_RESUMED, function (event, arg) {
  205. $scope.connections[arg.data.device].paused = false;
  206. });
  207. $scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
  208. $scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
  209. });
  210. $scope.$on(Events.CONFIG_SAVED, function (event, arg) {
  211. updateLocalConfig(arg.data);
  212. $http.get(urlbase + '/system/config/insync').success(function (data) {
  213. $scope.configInSync = data.configInSync;
  214. }).error($scope.emitHTTPError);
  215. });
  216. $scope.$on(Events.DOWNLOAD_PROGRESS, function (event, arg) {
  217. var stats = arg.data;
  218. var progress = {};
  219. for (var folder in stats) {
  220. progress[folder] = {};
  221. for (var file in stats[folder]) {
  222. var s = stats[folder][file];
  223. var reused = 100 * s.reused / s.total;
  224. var copiedFromOrigin = 100 * s.copiedFromOrigin / s.total;
  225. var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
  226. var pulled = 100 * s.pulled / s.total;
  227. var pulling = 100 * s.pulling / s.total;
  228. // We try to round up pulling to at least a percent so that it would be at least a bit visible.
  229. if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
  230. pulling = 1;
  231. }
  232. progress[folder][file] = {
  233. reused: reused,
  234. copiedFromOrigin: copiedFromOrigin,
  235. copiedFromElsewhere: copiedFromElsewhere,
  236. pulled: pulled,
  237. pulling: pulling,
  238. bytesTotal: s.bytesTotal,
  239. bytesDone: s.bytesDone,
  240. };
  241. }
  242. }
  243. for (var folder in $scope.progress) {
  244. if (!(folder in progress)) {
  245. if ($scope.neededFolder === folder) {
  246. refreshNeed(folder);
  247. }
  248. } else if ($scope.neededFolder === folder) {
  249. for (file in $scope.progress[folder]) {
  250. if (!(file in progress[folder])) {
  251. refreshNeed(folder);
  252. break;
  253. }
  254. }
  255. }
  256. }
  257. $scope.progress = progress;
  258. console.log("DownloadProgress", $scope.progress);
  259. });
  260. $scope.$on(Events.FOLDER_SUMMARY, function (event, arg) {
  261. var data = arg.data;
  262. $scope.model[data.folder] = data.summary;
  263. recalcLocalStateTotal();
  264. });
  265. $scope.$on(Events.FOLDER_COMPLETION, function (event, arg) {
  266. var data = arg.data;
  267. if (!$scope.completion[data.device]) {
  268. $scope.completion[data.device] = {};
  269. }
  270. $scope.completion[data.device][data.folder] = data.completion;
  271. var tot = 0,
  272. cnt = 0;
  273. for (var cmp in $scope.completion[data.device]) {
  274. if (cmp === "_total") {
  275. continue;
  276. }
  277. tot += $scope.completion[data.device][cmp];
  278. cnt += 1;
  279. }
  280. $scope.completion[data.device]._total = tot / cnt;
  281. });
  282. $scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
  283. var data = arg.data;
  284. $scope.failed[data.folder] = data.errors;
  285. });
  286. $scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
  287. var data = arg.data;
  288. $scope.scanProgress[data.folder] = {
  289. current: data.current,
  290. total: data.total,
  291. rate: data.rate
  292. };
  293. console.log("FolderScanProgress", data);
  294. });
  295. $scope.emitHTTPError = function (data, status, headers, config) {
  296. $scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
  297. };
  298. var debouncedFuncs = {};
  299. function refreshFolder(folder) {
  300. var key = "refreshFolder" + folder;
  301. if (!debouncedFuncs[key]) {
  302. debouncedFuncs[key] = debounce(function () {
  303. $http.get(urlbase + '/db/status?folder=' + encodeURIComponent(folder)).success(function (data) {
  304. $scope.model[folder] = data;
  305. recalcLocalStateTotal();
  306. console.log("refreshFolder", folder, data);
  307. }).error($scope.emitHTTPError);
  308. }, 1000, true);
  309. }
  310. debouncedFuncs[key]();
  311. }
  312. function updateLocalConfig(config) {
  313. var hasConfig = !isEmptyObject($scope.config);
  314. $scope.config = config;
  315. $scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', ');
  316. $scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
  317. $scope.devices = $scope.config.devices;
  318. $scope.devices.forEach(function (deviceCfg) {
  319. $scope.completion[deviceCfg.deviceID] = {
  320. _total: 100
  321. };
  322. });
  323. $scope.devices.sort(deviceCompare);
  324. $scope.folders = folderMap($scope.config.folders);
  325. Object.keys($scope.folders).forEach(function (folder) {
  326. refreshFolder(folder);
  327. $scope.folders[folder].devices.forEach(function (deviceCfg) {
  328. refreshCompletion(deviceCfg.deviceID, folder);
  329. });
  330. });
  331. // If we're not listening on localhost, and there is no
  332. // authentication configured, and the magic setting to silence the
  333. // warning isn't set, then yell at the user.
  334. var guiCfg = $scope.config.gui;
  335. $scope.openNoAuth = guiCfg.address.substr(0, 4) !== "127."
  336. && guiCfg.address.substr(0, 6) !== "[::1]:"
  337. && (!guiCfg.user || !guiCfg.password)
  338. && !guiCfg.insecureAdminAccess;
  339. if (!hasConfig) {
  340. $scope.$emit('ConfigLoaded');
  341. }
  342. }
  343. function refreshSystem() {
  344. $http.get(urlbase + '/system/status').success(function (data) {
  345. $scope.myID = data.myID;
  346. $scope.system = data;
  347. var listenersFailed = [];
  348. for (var address in data.connectionServiceStatus) {
  349. if (data.connectionServiceStatus[address].error) {
  350. listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
  351. }
  352. }
  353. $scope.listenersFailed = listenersFailed;
  354. $scope.listenersTotal = Object.keys(data.connectionServiceStatus).length;
  355. $scope.discoveryTotal = data.discoveryMethods;
  356. var discoveryFailed = [];
  357. for (var disco in data.discoveryErrors) {
  358. if (data.discoveryErrors[disco]) {
  359. discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
  360. }
  361. }
  362. $scope.discoveryFailed = discoveryFailed;
  363. console.log("refreshSystem", data);
  364. }).error($scope.emitHTTPError);
  365. }
  366. function recalcLocalStateTotal () {
  367. $scope.localStateTotal = {
  368. bytes: 0,
  369. files: 0
  370. };
  371. for (var f in $scope.model) {
  372. $scope.localStateTotal.bytes += $scope.model[f].localBytes;
  373. $scope.localStateTotal.files += $scope.model[f].localFiles;
  374. }
  375. }
  376. function refreshCompletion(device, folder) {
  377. if (device === $scope.myID) {
  378. return;
  379. }
  380. $http.get(urlbase + '/db/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) {
  381. if (!$scope.completion[device]) {
  382. $scope.completion[device] = {};
  383. }
  384. $scope.completion[device][folder] = data.completion;
  385. var tot = 0,
  386. cnt = 0;
  387. for (var cmp in $scope.completion[device]) {
  388. if (cmp === "_total") {
  389. continue;
  390. }
  391. tot += $scope.completion[device][cmp];
  392. cnt += 1;
  393. }
  394. $scope.completion[device]._total = tot / cnt;
  395. console.log("refreshCompletion", device, folder, $scope.completion[device]);
  396. }).error($scope.emitHTTPError);
  397. }
  398. function refreshConnectionStats() {
  399. $http.get(urlbase + '/system/connections').success(function (data) {
  400. var now = Date.now(),
  401. td = (now - prevDate) / 1000,
  402. id;
  403. prevDate = now;
  404. try {
  405. data.total.inbps = Math.max(0, (data.total.inBytesTotal - $scope.connectionsTotal.inBytesTotal) / td);
  406. data.total.outbps = Math.max(0, (data.total.outBytesTotal - $scope.connectionsTotal.outBytesTotal) / td);
  407. } catch (e) {
  408. data.total.inbps = 0;
  409. data.total.outbps = 0;
  410. }
  411. $scope.connectionsTotal = data.total;
  412. data = data.connections;
  413. for (id in data) {
  414. if (!data.hasOwnProperty(id)) {
  415. continue;
  416. }
  417. try {
  418. data[id].inbps = Math.max(0, (data[id].inBytesTotal - $scope.connections[id].inBytesTotal) / td);
  419. data[id].outbps = Math.max(0, (data[id].outBytesTotal - $scope.connections[id].outBytesTotal) / td);
  420. } catch (e) {
  421. data[id].inbps = 0;
  422. data[id].outbps = 0;
  423. }
  424. }
  425. $scope.connections = data;
  426. console.log("refreshConnections", data);
  427. }).error($scope.emitHTTPError);
  428. }
  429. function refreshErrors() {
  430. $http.get(urlbase + '/system/error').success(function (data) {
  431. $scope.errors = data.errors;
  432. console.log("refreshErrors", data);
  433. }).error($scope.emitHTTPError);
  434. }
  435. function refreshConfig() {
  436. $http.get(urlbase + '/system/config').success(function (data) {
  437. updateLocalConfig(data);
  438. console.log("refreshConfig", data);
  439. }).error($scope.emitHTTPError);
  440. $http.get(urlbase + '/system/config/insync').success(function (data) {
  441. $scope.configInSync = data.configInSync;
  442. }).error($scope.emitHTTPError);
  443. }
  444. function refreshNeed(folder) {
  445. var url = urlbase + "/db/need?folder=" + encodeURIComponent(folder);
  446. url += "&page=" + $scope.neededCurrentPage;
  447. url += "&perpage=" + $scope.neededPageSize;
  448. $http.get(url).success(function (data) {
  449. if ($scope.neededFolder === folder) {
  450. console.log("refreshNeed", folder, data);
  451. parseNeeded(data);
  452. }
  453. }).error($scope.emitHTTPError);
  454. }
  455. function needAction(file) {
  456. var fDelete = 4096;
  457. var fDirectory = 16384;
  458. if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
  459. return 'rmdir';
  460. } else if ((file.flags & fDelete) === fDelete) {
  461. return 'rm';
  462. } else if ((file.flags & fDirectory) === fDirectory) {
  463. return 'touch';
  464. } else {
  465. return 'sync';
  466. }
  467. }
  468. function parseNeeded(data) {
  469. var merged = [];
  470. data.progress.forEach(function (item) {
  471. item.type = "progress";
  472. item.action = needAction(item);
  473. merged.push(item);
  474. });
  475. data.queued.forEach(function (item) {
  476. item.type = "queued";
  477. item.action = needAction(item);
  478. merged.push(item);
  479. });
  480. data.rest.forEach(function (item) {
  481. item.type = "rest";
  482. item.action = needAction(item);
  483. merged.push(item);
  484. });
  485. $scope.needed = merged;
  486. $scope.neededTotal = data.total;
  487. }
  488. $scope.neededPageChanged = function (page) {
  489. $scope.neededCurrentPage = page;
  490. refreshNeed($scope.neededFolder);
  491. };
  492. $scope.neededChangePageSize = function (perpage) {
  493. $scope.neededPageSize = perpage;
  494. refreshNeed($scope.neededFolder);
  495. };
  496. $scope.failedPageChanged = function (page) {
  497. $scope.failedCurrentPage = page;
  498. };
  499. $scope.failedChangePageSize = function (perpage) {
  500. $scope.failedPageSize = perpage;
  501. };
  502. var refreshDeviceStats = debounce(function () {
  503. $http.get(urlbase + "/stats/device").success(function (data) {
  504. $scope.deviceStats = data;
  505. for (var device in $scope.deviceStats) {
  506. $scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
  507. $scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
  508. }
  509. console.log("refreshDeviceStats", data);
  510. }).error($scope.emitHTTPError);
  511. }, 2500);
  512. var refreshFolderStats = debounce(function () {
  513. $http.get(urlbase + "/stats/folder").success(function (data) {
  514. $scope.folderStats = data;
  515. for (var folder in $scope.folderStats) {
  516. if ($scope.folderStats[folder].lastFile) {
  517. $scope.folderStats[folder].lastFile.at = new Date($scope.folderStats[folder].lastFile.at);
  518. }
  519. $scope.folderStats[folder].lastScan = new Date($scope.folderStats[folder].lastScan);
  520. $scope.folderStats[folder].lastScanDays = (new Date() - $scope.folderStats[folder].lastScan) / 1000 / 86400;
  521. }
  522. console.log("refreshfolderStats", data);
  523. }).error($scope.emitHTTPError);
  524. }, 2500);
  525. var refreshThemes = debounce(function () {
  526. $http.get("themes.json").success(function (data) { // no urlbase here as this is served by the asset handler
  527. $scope.themes = data.themes;
  528. }).error($scope.emitHTTPError);
  529. }, 2500);
  530. $scope.refresh = function () {
  531. refreshSystem();
  532. refreshConnectionStats();
  533. refreshErrors();
  534. };
  535. $scope.folderStatus = function (folderCfg) {
  536. if (typeof $scope.model[folderCfg.id] === 'undefined') {
  537. return 'unknown';
  538. }
  539. // after restart syncthing process state may be empty
  540. if (!$scope.model[folderCfg.id].state) {
  541. return 'unknown';
  542. }
  543. if ($scope.model[folderCfg.id].invalid) {
  544. return 'stopped';
  545. }
  546. var state = '' + $scope.model[folderCfg.id].state;
  547. if (state === 'error') {
  548. return 'stopped'; // legacy, the state is called "stopped" in the GUI
  549. }
  550. if (state === 'idle' && $scope.model[folderCfg.id].needFiles > 0) {
  551. return 'outofsync';
  552. }
  553. if (state === 'scanning') {
  554. return state;
  555. }
  556. if (folderCfg.devices.length <= 1) {
  557. return 'unshared';
  558. }
  559. return state;
  560. };
  561. $scope.folderClass = function (folderCfg) {
  562. var status = $scope.folderStatus(folderCfg);
  563. if (status === 'idle') {
  564. return 'success';
  565. }
  566. if (status === 'syncing' || status === 'scanning') {
  567. return 'primary';
  568. }
  569. if (status === 'unknown') {
  570. return 'info';
  571. }
  572. if (status === 'stopped' || status === 'outofsync' || status === 'error') {
  573. return 'danger';
  574. }
  575. if (status === 'unshared') {
  576. return 'warning';
  577. }
  578. return 'info';
  579. };
  580. $scope.syncPercentage = function (folder) {
  581. if (typeof $scope.model[folder] === 'undefined') {
  582. return 100;
  583. }
  584. if ($scope.model[folder].globalBytes === 0) {
  585. return 100;
  586. }
  587. var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes;
  588. return Math.floor(pct);
  589. };
  590. $scope.scanPercentage = function (folder) {
  591. if (!$scope.scanProgress[folder]) {
  592. return undefined;
  593. }
  594. var pct = 100 * $scope.scanProgress[folder].current / $scope.scanProgress[folder].total;
  595. return Math.floor(pct);
  596. };
  597. $scope.scanRate = function (folder) {
  598. if (!$scope.scanProgress[folder]) {
  599. return 0;
  600. }
  601. return $scope.scanProgress[folder].rate;
  602. };
  603. $scope.scanRemaining = function (folder) {
  604. // Formats the remaining scan time as a string. Includes days and
  605. // hours only when relevant, resulting in time stamps like:
  606. // 00m 40s
  607. // 32m 40s
  608. // 2h 32m
  609. // 4d 2h
  610. if (!$scope.scanProgress[folder]) {
  611. return "";
  612. }
  613. // Calculate remaining bytes and seconds based on our current
  614. // rate.
  615. var remainingBytes = $scope.scanProgress[folder].total - $scope.scanProgress[folder].current;
  616. var seconds = remainingBytes / $scope.scanProgress[folder].rate;
  617. // Round up to closest ten seconds to avoid flapping too much to
  618. // and fro.
  619. seconds = Math.ceil(seconds / 10) * 10;
  620. // Separate out the number of days.
  621. var days = 0;
  622. var res = [];
  623. if (seconds >= 86400) {
  624. days = Math.floor(seconds / 86400);
  625. res.push('' + days + 'd')
  626. seconds = seconds % 86400;
  627. }
  628. // Separate out the number of hours.
  629. var hours = 0;
  630. if (seconds > 3600) {
  631. hours = Math.floor(seconds / 3600);
  632. res.push('' + hours + 'h')
  633. seconds = seconds % 3600;
  634. }
  635. var d = new Date(1970, 0, 1).setSeconds(seconds);
  636. if (days === 0) {
  637. // Format minutes only if we're within a day of completion.
  638. var f = $filter('date')(d, "m'm'");
  639. res.push(f);
  640. }
  641. if (days === 0 && hours === 0) {
  642. // Format seconds only when we're within an hour of completion.
  643. var f = $filter('date')(d, "ss's'");
  644. res.push(f);
  645. }
  646. return res.join(' ');
  647. };
  648. $scope.deviceStatus = function (deviceCfg) {
  649. if ($scope.deviceFolders(deviceCfg).length === 0) {
  650. return 'unused';
  651. }
  652. if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
  653. return 'unknown';
  654. }
  655. if ($scope.connections[deviceCfg.deviceID].paused) {
  656. return 'paused';
  657. }
  658. if ($scope.connections[deviceCfg.deviceID].connected) {
  659. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  660. return 'insync';
  661. } else {
  662. return 'syncing';
  663. }
  664. }
  665. // Disconnected
  666. return 'disconnected';
  667. };
  668. $scope.deviceClass = function (deviceCfg) {
  669. if ($scope.deviceFolders(deviceCfg).length === 0) {
  670. // Unused
  671. return 'warning';
  672. }
  673. if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
  674. return 'info';
  675. }
  676. if ($scope.connections[deviceCfg.deviceID].paused) {
  677. return 'default';
  678. }
  679. if ($scope.connections[deviceCfg.deviceID].connected) {
  680. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  681. return 'success';
  682. } else {
  683. return 'primary';
  684. }
  685. }
  686. // Disconnected
  687. return 'info';
  688. };
  689. $scope.syncthingStatus = function () {
  690. var syncCount = 0;
  691. var notifyCount = 0;
  692. var pauseCount = 0;
  693. // loop through all folders
  694. var folderListCache = $scope.folderList();
  695. for (var i = 0; i < folderListCache.length; i++) {
  696. var status = $scope.folderStatus(folderListCache[i]);
  697. switch (status) {
  698. case 'syncing':
  699. syncCount++;
  700. break;
  701. case 'stopped':
  702. case 'unknown':
  703. case 'outofsync':
  704. case 'error':
  705. notifyCount++;
  706. break;
  707. }
  708. }
  709. // loop through all devices
  710. var deviceCount = $scope.devices.length;
  711. for (var i = 0; i < $scope.devices.length; i++) {
  712. var status = $scope.deviceStatus({
  713. deviceID:$scope.devices[i].deviceID
  714. });
  715. switch (status) {
  716. case 'unknown':
  717. notifyCount++;
  718. break;
  719. case 'paused':
  720. pauseCount++;
  721. break;
  722. case 'unused':
  723. deviceCount--;
  724. break;
  725. }
  726. }
  727. // enumerate notifications
  728. if ($scope.openNoAuth || !$scope.configInSync || Object.keys($scope.deviceRejections).length > 0 || Object.keys($scope.folderRejections).length > 0 || $scope.errorList().length > 0 || !online) {
  729. notifyCount++;
  730. }
  731. // at least one folder is syncing
  732. if (syncCount > 0) {
  733. return 'sync';
  734. }
  735. // a device is unknown or a folder is stopped/unknown/outofsync/error or some other notification is open or gui offline
  736. if (notifyCount > 0) {
  737. return 'notify';
  738. }
  739. // all used devices are paused except (this) one
  740. if (pauseCount === deviceCount-1) {
  741. return 'pause';
  742. }
  743. return 'default';
  744. };
  745. $scope.deviceAddr = function (deviceCfg) {
  746. var conn = $scope.connections[deviceCfg.deviceID];
  747. if (conn && conn.connected) {
  748. return conn.address;
  749. }
  750. return '?';
  751. };
  752. $scope.deviceCompletion = function (deviceCfg) {
  753. var conn = $scope.connections[deviceCfg.deviceID];
  754. if (conn) {
  755. return conn.completion + '%';
  756. }
  757. return '';
  758. };
  759. $scope.findDevice = function (deviceID) {
  760. var matches = $scope.devices.filter(function (n) {
  761. return n.deviceID === deviceID;
  762. });
  763. if (matches.length !== 1) {
  764. return undefined;
  765. }
  766. return matches[0];
  767. };
  768. $scope.deviceName = function (deviceCfg) {
  769. if (typeof deviceCfg === 'undefined' || typeof deviceCfg.deviceID === 'undefined') {
  770. return "";
  771. }
  772. if (deviceCfg.name) {
  773. return deviceCfg.name;
  774. }
  775. return deviceCfg.deviceID.substr(0, 6);
  776. };
  777. $scope.thisDeviceName = function () {
  778. var device = $scope.thisDevice();
  779. if (typeof device === 'undefined') {
  780. return "(unknown device)";
  781. }
  782. if (device.name) {
  783. return device.name;
  784. }
  785. return device.deviceID.substr(0, 6);
  786. };
  787. $scope.pauseDevice = function (device) {
  788. $http.post(urlbase + "/system/pause?device=" + device);
  789. };
  790. $scope.resumeDevice = function (device) {
  791. $http.post(urlbase + "/system/resume?device=" + device);
  792. };
  793. $scope.editSettings = function () {
  794. // Make a working copy
  795. $scope.tmpOptions = angular.copy($scope.config.options);
  796. $scope.tmpOptions.urEnabled = ($scope.tmpOptions.urAccepted > 0);
  797. $scope.tmpOptions.deviceName = $scope.thisDevice().name;
  798. $scope.tmpOptions.autoUpgradeEnabled = ($scope.tmpOptions.autoUpgradeIntervalH > 0);
  799. $scope.tmpGUI = angular.copy($scope.config.gui);
  800. $('#settings').modal();
  801. };
  802. $scope.saveConfig = function () {
  803. var cfg = JSON.stringify($scope.config);
  804. var opts = {
  805. headers: {
  806. 'Content-Type': 'application/json'
  807. }
  808. };
  809. $http.post(urlbase + '/system/config', cfg, opts).success(function () {
  810. $http.get(urlbase + '/system/config/insync').success(function (data) {
  811. $scope.configInSync = data.configInSync;
  812. });
  813. }).error($scope.emitHTTPError);
  814. };
  815. $scope.saveSettings = function () {
  816. // Make sure something changed
  817. var changed = !angular.equals($scope.config.options, $scope.tmpOptions) || !angular.equals($scope.config.gui, $scope.tmpGUI);
  818. var themeChanged = $scope.config.gui.theme !== $scope.tmpGUI.theme;
  819. if (changed) {
  820. // Check if usage reporting has been enabled or disabled
  821. if ($scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted <= 0) {
  822. $scope.tmpOptions.urAccepted = 1000;
  823. } else if (!$scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted > 0) {
  824. $scope.tmpOptions.urAccepted = -1;
  825. }
  826. // Check if auto-upgrade has been enabled or disabled
  827. if ($scope.tmpOptions.autoUpgradeEnabled) {
  828. $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
  829. } else {
  830. $scope.tmpOptions.autoUpgradeIntervalH = 0;
  831. }
  832. // Check if protocol will need to be changed on restart
  833. if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) {
  834. $scope.protocolChanged = true;
  835. }
  836. // Apply new settings locally
  837. $scope.thisDevice().name = $scope.tmpOptions.deviceName;
  838. $scope.config.options = angular.copy($scope.tmpOptions);
  839. $scope.config.gui = angular.copy($scope.tmpGUI);
  840. ['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
  841. $scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
  842. return x.trim();
  843. });
  844. });
  845. $scope.saveConfig();
  846. }
  847. $('#settings').modal("hide");
  848. if (themeChanged) {
  849. document.location.reload(true);
  850. }
  851. };
  852. $scope.saveAdvanced = function () {
  853. $scope.config = $scope.advancedConfig;
  854. $scope.saveConfig();
  855. $('#advanced').modal("hide");
  856. };
  857. $scope.restart = function () {
  858. restarting = true;
  859. $('#restarting').modal();
  860. $http.post(urlbase + '/system/restart');
  861. $scope.configInSync = true;
  862. // Switch webpage protocol if needed
  863. if ($scope.protocolChanged) {
  864. var protocol = 'http';
  865. if ($scope.config.gui.useTLS) {
  866. protocol = 'https';
  867. }
  868. setTimeout(function () {
  869. window.location.protocol = protocol;
  870. }, 2500);
  871. $scope.protocolChanged = false;
  872. }
  873. };
  874. $scope.upgrade = function () {
  875. restarting = true;
  876. $('#majorUpgrade').modal('hide');
  877. $('#upgrading').modal();
  878. $http.post(urlbase + '/system/upgrade').success(function () {
  879. $('#restarting').modal();
  880. $('#upgrading').modal('hide');
  881. }).error(function () {
  882. $('#upgrading').modal('hide');
  883. });
  884. };
  885. $scope.shutdown = function () {
  886. restarting = true;
  887. $http.post(urlbase + '/system/shutdown').success(function () {
  888. $('#shutdown').modal();
  889. }).error($scope.emitHTTPError);
  890. $scope.configInSync = true;
  891. };
  892. $scope.editDevice = function (deviceCfg) {
  893. $scope.currentDevice = $.extend({}, deviceCfg);
  894. $scope.editingExisting = true;
  895. $scope.currentDevice._addressesStr = deviceCfg.addresses.join(', ');
  896. $scope.currentDevice.selectedFolders = {};
  897. $scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
  898. $scope.currentDevice.selectedFolders[folder] = true;
  899. });
  900. $scope.deviceEditor.$setPristine();
  901. $('#editDevice').modal();
  902. };
  903. $scope.addDevice = function (deviceID, name) {
  904. return $http.get(urlbase + '/system/discovery')
  905. .success(function (registry) {
  906. $scope.discovery = registry;
  907. })
  908. .then(function () {
  909. $scope.currentDevice = {
  910. name: name,
  911. deviceID: deviceID,
  912. _addressesStr: 'dynamic',
  913. compression: 'metadata',
  914. introducer: false,
  915. selectedFolders: {}
  916. };
  917. $scope.editingExisting = false;
  918. $scope.deviceEditor.$setPristine();
  919. $('#editDevice').modal();
  920. });
  921. };
  922. $scope.deleteDevice = function () {
  923. $('#editDevice').modal('hide');
  924. if (!$scope.editingExisting) {
  925. return;
  926. }
  927. $scope.devices = $scope.devices.filter(function (n) {
  928. return n.deviceID !== $scope.currentDevice.deviceID;
  929. });
  930. $scope.config.devices = $scope.devices;
  931. // In case we later added the device manually, remove the ignoral
  932. // record.
  933. $scope.config.ignoredDevices = $scope.config.ignoredDevices.filter(function (id) {
  934. return id !== $scope.currentDevice.deviceID;
  935. });
  936. for (var id in $scope.folders) {
  937. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  938. return n.deviceID !== $scope.currentDevice.deviceID;
  939. });
  940. }
  941. $scope.saveConfig();
  942. };
  943. $scope.saveDevice = function () {
  944. $('#editDevice').modal('hide');
  945. $scope.saveDeviceConfig($scope.currentDevice);
  946. $scope.dismissDeviceRejection($scope.currentDevice.deviceID);
  947. };
  948. $scope.saveDeviceConfig = function (deviceCfg) {
  949. deviceCfg.addresses = deviceCfg._addressesStr.split(',').map(function (x) {
  950. return x.trim();
  951. });
  952. var done = false;
  953. for (var i = 0; i < $scope.devices.length && !done; i++) {
  954. if ($scope.devices[i].deviceID === deviceCfg.deviceID) {
  955. $scope.devices[i] = deviceCfg;
  956. done = true;
  957. }
  958. }
  959. if (!done) {
  960. $scope.devices.push(deviceCfg);
  961. }
  962. $scope.devices.sort(deviceCompare);
  963. $scope.config.devices = $scope.devices;
  964. // In case we are adding the device manually, remove the ignoral
  965. // record.
  966. $scope.config.ignoredDevices = $scope.config.ignoredDevices.filter(function (id) {
  967. return id !== deviceCfg.deviceID;
  968. });
  969. for (var id in deviceCfg.selectedFolders) {
  970. if (deviceCfg.selectedFolders[id]) {
  971. var found = false;
  972. for (i = 0; i < $scope.folders[id].devices.length; i++) {
  973. if ($scope.folders[id].devices[i].deviceID === deviceCfg.deviceID) {
  974. found = true;
  975. break;
  976. }
  977. }
  978. if (!found) {
  979. $scope.folders[id].devices.push({
  980. deviceID: deviceCfg.deviceID
  981. });
  982. }
  983. } else {
  984. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  985. return n.deviceID !== deviceCfg.deviceID;
  986. });
  987. }
  988. }
  989. $scope.saveConfig();
  990. };
  991. $scope.dismissDeviceRejection = function (device) {
  992. delete $scope.deviceRejections[device];
  993. };
  994. $scope.ignoreRejectedDevice = function (device) {
  995. $scope.config.ignoredDevices.push(device);
  996. $scope.saveConfig();
  997. $scope.dismissDeviceRejection(device);
  998. };
  999. $scope.otherDevices = function () {
  1000. return $scope.devices.filter(function (n) {
  1001. return n.deviceID !== $scope.myID;
  1002. });
  1003. };
  1004. $scope.thisDevice = function () {
  1005. for (var i = 0; i < $scope.devices.length; i++) {
  1006. var n = $scope.devices[i];
  1007. if (n.deviceID === $scope.myID) {
  1008. return n;
  1009. }
  1010. }
  1011. };
  1012. $scope.allDevices = function () {
  1013. var devices = $scope.otherDevices();
  1014. devices.push($scope.thisDevice());
  1015. return devices;
  1016. };
  1017. $scope.errorList = function () {
  1018. if (!$scope.errors) {
  1019. return [];
  1020. }
  1021. return $scope.errors.filter(function (e) {
  1022. return e.when > $scope.seenError;
  1023. });
  1024. };
  1025. $scope.clearErrors = function () {
  1026. $scope.seenError = $scope.errors[$scope.errors.length - 1].when;
  1027. $http.post(urlbase + '/system/error/clear');
  1028. };
  1029. $scope.friendlyDevices = function (str) {
  1030. for (var i = 0; i < $scope.devices.length; i++) {
  1031. var cfg = $scope.devices[i];
  1032. str = str.replace(cfg.deviceID, $scope.deviceName(cfg));
  1033. }
  1034. return str;
  1035. };
  1036. $scope.folderList = function () {
  1037. return folderList($scope.folders);
  1038. };
  1039. $scope.directoryList = [];
  1040. $scope.$watch('currentFolder.path', function (newvalue) {
  1041. if (newvalue && newvalue.trim().charAt(0) === '~') {
  1042. $scope.currentFolder.path = $scope.system.tilde + newvalue.trim().substring(1);
  1043. }
  1044. $http.get(urlbase + '/system/browse', {
  1045. params: { current: newvalue }
  1046. }).success(function (data) {
  1047. $scope.directoryList = data;
  1048. }).error($scope.emitHTTPError);
  1049. });
  1050. $scope.loadFormIntoScope = function (form) {
  1051. console.log('loadFormIntoScope',form.$name);
  1052. switch (form.$name) {
  1053. case 'deviceEditor':
  1054. $scope.deviceEditor = form;
  1055. break;
  1056. case 'folderEditor':
  1057. $scope.folderEditor = form;
  1058. break;
  1059. }
  1060. }
  1061. $scope.editFolder = function (folderCfg) {
  1062. $scope.currentFolder = angular.copy(folderCfg);
  1063. if ($scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
  1064. $scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
  1065. }
  1066. $scope.currentFolder.selectedDevices = {};
  1067. $scope.currentFolder.devices.forEach(function (n) {
  1068. $scope.currentFolder.selectedDevices[n.deviceID] = true;
  1069. });
  1070. if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "trashcan") {
  1071. $scope.currentFolder.trashcanFileVersioning = true;
  1072. $scope.currentFolder.fileVersioningSelector = "trashcan";
  1073. $scope.currentFolder.trashcanClean = +$scope.currentFolder.versioning.params.cleanoutDays;
  1074. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "simple") {
  1075. $scope.currentFolder.simpleFileVersioning = true;
  1076. $scope.currentFolder.fileVersioningSelector = "simple";
  1077. $scope.currentFolder.simpleKeep = +$scope.currentFolder.versioning.params.keep;
  1078. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "staggered") {
  1079. $scope.currentFolder.staggeredFileVersioning = true;
  1080. $scope.currentFolder.fileVersioningSelector = "staggered";
  1081. $scope.currentFolder.staggeredMaxAge = Math.floor(+$scope.currentFolder.versioning.params.maxAge / 86400);
  1082. $scope.currentFolder.staggeredCleanInterval = +$scope.currentFolder.versioning.params.cleanInterval;
  1083. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.versioning.params.versionsPath;
  1084. } else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "external") {
  1085. $scope.currentFolder.externalFileVersioning = true;
  1086. $scope.currentFolder.fileVersioningSelector = "external";
  1087. $scope.currentFolder.externalCommand = $scope.currentFolder.versioning.params.command;
  1088. } else {
  1089. $scope.currentFolder.fileVersioningSelector = "none";
  1090. }
  1091. $scope.currentFolder.trashcanClean = $scope.currentFolder.trashcanClean || 0; // weeds out nulls and undefineds
  1092. $scope.currentFolder.simpleKeep = $scope.currentFolder.simpleKeep || 5;
  1093. $scope.currentFolder.staggeredCleanInterval = $scope.currentFolder.staggeredCleanInterval || 3600;
  1094. $scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.staggeredVersionsPath || "";
  1095. // staggeredMaxAge can validly be zero, which we should not replace
  1096. // with the default value of 365. So only set the default if it's
  1097. // actually undefined.
  1098. if (typeof $scope.currentFolder.staggeredMaxAge === 'undefined') {
  1099. $scope.currentFolder.staggeredMaxAge = 365;
  1100. }
  1101. $scope.currentFolder.externalCommand = $scope.currentFolder.externalCommand || "";
  1102. $scope.editingExisting = true;
  1103. $scope.folderEditor.$setPristine();
  1104. $('#editFolder').modal();
  1105. };
  1106. $scope.addFolder = function () {
  1107. $scope.currentFolder = {
  1108. selectedDevices: {},
  1109. type: "readwrite",
  1110. rescanIntervalS: 60,
  1111. minDiskFreePct: 1,
  1112. maxConflicts: 10,
  1113. order: "random",
  1114. fileVersioningSelector: "none",
  1115. trashcanClean: 0,
  1116. simpleKeep: 5,
  1117. staggeredMaxAge: 365,
  1118. staggeredCleanInterval: 3600,
  1119. staggeredVersionsPath: "",
  1120. externalCommand: "",
  1121. autoNormalize: true
  1122. };
  1123. $scope.editingExisting = false;
  1124. $scope.folderEditor.$setPristine();
  1125. $http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
  1126. $scope.currentFolder.id = data.random.substr(0, 5) + '-' + data.random.substr(5, 5);
  1127. $('#editFolder').modal();
  1128. });
  1129. };
  1130. $scope.addFolderAndShare = function (folder, folderLabel, device) {
  1131. $scope.dismissFolderRejection(folder, device);
  1132. $scope.currentFolder = {
  1133. id: folder,
  1134. label: folderLabel,
  1135. selectedDevices: {},
  1136. rescanIntervalS: 60,
  1137. minDiskFreePct: 1,
  1138. maxConflicts: 10,
  1139. order: "random",
  1140. fileVersioningSelector: "none",
  1141. trashcanClean: 0,
  1142. simpleKeep: 5,
  1143. staggeredMaxAge: 365,
  1144. staggeredCleanInterval: 3600,
  1145. staggeredVersionsPath: "",
  1146. externalCommand: "",
  1147. autoNormalize: true,
  1148. viewFlags: {
  1149. importFromOtherDevice: true
  1150. }
  1151. };
  1152. $scope.currentFolder.selectedDevices[device] = true;
  1153. $scope.editingExisting = false;
  1154. $scope.folderEditor.$setPristine();
  1155. $('#editFolder').modal();
  1156. };
  1157. $scope.shareFolderWithDevice = function (folder, device) {
  1158. $scope.folders[folder].devices.push({
  1159. deviceID: device
  1160. });
  1161. $scope.config.folders = folderList($scope.folders);
  1162. $scope.saveConfig();
  1163. $scope.dismissFolderRejection(folder, device);
  1164. };
  1165. $scope.saveFolder = function () {
  1166. $('#editFolder').modal('hide');
  1167. var folderCfg = $scope.currentFolder;
  1168. folderCfg.devices = [];
  1169. folderCfg.selectedDevices[$scope.myID] = true;
  1170. for (var deviceID in folderCfg.selectedDevices) {
  1171. if (folderCfg.selectedDevices[deviceID] === true) {
  1172. folderCfg.devices.push({
  1173. deviceID: deviceID
  1174. });
  1175. }
  1176. }
  1177. delete folderCfg.selectedDevices;
  1178. if (folderCfg.fileVersioningSelector === "trashcan") {
  1179. folderCfg.versioning = {
  1180. 'Type': 'trashcan',
  1181. 'Params': {
  1182. 'cleanoutDays': '' + folderCfg.trashcanClean
  1183. }
  1184. };
  1185. delete folderCfg.trashcanFileVersioning;
  1186. delete folderCfg.trashcanClean;
  1187. } else if (folderCfg.fileVersioningSelector === "simple") {
  1188. folderCfg.versioning = {
  1189. 'Type': 'simple',
  1190. 'Params': {
  1191. 'keep': '' + folderCfg.simpleKeep
  1192. }
  1193. };
  1194. delete folderCfg.simpleFileVersioning;
  1195. delete folderCfg.simpleKeep;
  1196. } else if (folderCfg.fileVersioningSelector === "staggered") {
  1197. folderCfg.versioning = {
  1198. 'type': 'staggered',
  1199. 'params': {
  1200. 'maxAge': '' + (folderCfg.staggeredMaxAge * 86400),
  1201. 'cleanInterval': '' + folderCfg.staggeredCleanInterval,
  1202. 'versionsPath': '' + folderCfg.staggeredVersionsPath
  1203. }
  1204. };
  1205. delete folderCfg.staggeredFileVersioning;
  1206. delete folderCfg.staggeredMaxAge;
  1207. delete folderCfg.staggeredCleanInterval;
  1208. delete folderCfg.staggeredVersionsPath;
  1209. } else if (folderCfg.fileVersioningSelector === "external") {
  1210. folderCfg.versioning = {
  1211. 'Type': 'external',
  1212. 'Params': {
  1213. 'command': '' + folderCfg.externalCommand
  1214. }
  1215. };
  1216. delete folderCfg.externalFileVersioning;
  1217. delete folderCfg.externalCommand;
  1218. } else {
  1219. delete folderCfg.versioning;
  1220. }
  1221. $scope.folders[folderCfg.id] = folderCfg;
  1222. $scope.config.folders = folderList($scope.folders);
  1223. $scope.saveConfig();
  1224. };
  1225. $scope.dismissFolderRejection = function (folder, device) {
  1226. delete $scope.folderRejections[folder + "-" + device];
  1227. };
  1228. $scope.sharesFolder = function (folderCfg) {
  1229. var names = [];
  1230. folderCfg.devices.forEach(function (device) {
  1231. if (device.deviceID !== $scope.myID) {
  1232. names.push($scope.deviceName($scope.findDevice(device.deviceID)));
  1233. }
  1234. });
  1235. names.sort();
  1236. return names.join(", ");
  1237. };
  1238. $scope.deviceFolders = function (deviceCfg) {
  1239. var folders = [];
  1240. for (var folderID in $scope.folders) {
  1241. var devices = $scope.folders[folderID].devices;
  1242. for (var i = 0; i < devices.length; i++) {
  1243. if (devices[i].deviceID === deviceCfg.deviceID) {
  1244. folders.push(folderID);
  1245. break;
  1246. }
  1247. }
  1248. }
  1249. folders.sort();
  1250. return folders;
  1251. };
  1252. $scope.folderLabel = function (folderID) {
  1253. var label = $scope.folders[folderID].label;
  1254. return label.length > 0 ? label : folderID;
  1255. }
  1256. $scope.deleteFolder = function (id) {
  1257. $('#editFolder').modal('hide');
  1258. if (!$scope.editingExisting) {
  1259. return;
  1260. }
  1261. delete $scope.folders[id];
  1262. delete $scope.model[id];
  1263. $scope.config.folders = folderList($scope.folders);
  1264. recalcLocalStateTotal();
  1265. $scope.saveConfig();
  1266. };
  1267. $scope.editIgnores = function () {
  1268. if (!$scope.editingExisting) {
  1269. return;
  1270. }
  1271. $('#editIgnoresButton').attr('disabled', 'disabled');
  1272. $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
  1273. .success(function (data) {
  1274. data.ignore = data.ignore || [];
  1275. var textArea = $('#editIgnores textarea');
  1276. textArea.val(data.ignore.join('\n'));
  1277. $('#editIgnores').modal()
  1278. .one('shown.bs.modal', function () {
  1279. textArea.focus();
  1280. });
  1281. })
  1282. .then(function () {
  1283. $('#editIgnoresButton').removeAttr('disabled');
  1284. });
  1285. };
  1286. $scope.saveIgnores = function () {
  1287. if (!$scope.editingExisting) {
  1288. return;
  1289. }
  1290. $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
  1291. ignore: $('#editIgnores textarea').val().split('\n')
  1292. });
  1293. };
  1294. $scope.setAPIKey = function (cfg) {
  1295. $http.get(urlbase + '/svc/random/string?length=32').success(function (data) {
  1296. cfg.apiKey = data.random;
  1297. });
  1298. };
  1299. $scope.acceptUR = function () {
  1300. $scope.config.options.urAccepted = 1000; // Larger than the largest existing report version
  1301. $scope.saveConfig();
  1302. $('#ur').modal('hide');
  1303. };
  1304. $scope.declineUR = function () {
  1305. $scope.config.options.urAccepted = -1;
  1306. $scope.saveConfig();
  1307. $('#ur').modal('hide');
  1308. };
  1309. $scope.showNeed = function (folder) {
  1310. $scope.neededFolder = folder;
  1311. refreshNeed(folder);
  1312. $('#needed').modal().on('hidden.bs.modal', function () {
  1313. $scope.neededFolder = undefined;
  1314. $scope.needed = undefined;
  1315. $scope.neededTotal = 0;
  1316. $scope.neededCurrentPage = 1;
  1317. });
  1318. };
  1319. $scope.showFailed = function (folder) {
  1320. $scope.failedCurrent = $scope.failed[folder];
  1321. $('#failed').modal().on('hidden.bs.modal', function () {
  1322. $scope.failedCurrent = undefined;
  1323. });
  1324. };
  1325. $scope.hasFailedFiles = function (folder) {
  1326. if (!$scope.failed[folder]) {
  1327. return false;
  1328. }
  1329. if ($scope.failed[folder].length === 0) {
  1330. return false;
  1331. }
  1332. return true;
  1333. };
  1334. $scope.override = function (folder) {
  1335. $http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
  1336. };
  1337. $scope.advanced = function () {
  1338. $scope.advancedConfig = angular.copy($scope.config);
  1339. $('#advanced').modal('show');
  1340. };
  1341. $scope.showReportPreview = function () {
  1342. $scope.reportPreview = true;
  1343. };
  1344. $scope.rescanAllFolders = function () {
  1345. $http.post(urlbase + "/db/scan");
  1346. };
  1347. $scope.rescanFolder = function (folder) {
  1348. $http.post(urlbase + "/db/scan?folder=" + encodeURIComponent(folder));
  1349. };
  1350. $scope.bumpFile = function (folder, file) {
  1351. var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
  1352. // In order to get the right view of data in the response.
  1353. url += "&page=" + $scope.neededCurrentPage;
  1354. url += "&perpage=" + $scope.neededPageSize;
  1355. $http.post(url).success(function (data) {
  1356. if ($scope.neededFolder === folder) {
  1357. console.log("bumpFile", folder, data);
  1358. parseNeeded(data);
  1359. }
  1360. }).error($scope.emitHTTPError);
  1361. };
  1362. $scope.versionString = function () {
  1363. if (!$scope.version.version) {
  1364. return '';
  1365. }
  1366. var os = {
  1367. 'darwin': 'Mac OS X',
  1368. 'dragonfly': 'DragonFly BSD',
  1369. 'freebsd': 'FreeBSD',
  1370. 'openbsd': 'OpenBSD',
  1371. 'netbsd': 'NetBSD',
  1372. 'linux': 'Linux',
  1373. 'windows': 'Windows',
  1374. 'solaris': 'Solaris'
  1375. }[$scope.version.os] || $scope.version.os;
  1376. var arch ={
  1377. '386': '32 bit',
  1378. 'amd64': '64 bit',
  1379. 'arm': 'ARM',
  1380. 'arm64': 'AArch64',
  1381. 'ppc64': 'PowerPC',
  1382. 'ppc64le': 'PowerPC (LE)'
  1383. }[$scope.version.arch] || $scope.version.arch;
  1384. return $scope.version.version + ', ' + os + ' (' + arch + ')';
  1385. };
  1386. $scope.inputTypeFor = function (key, value) {
  1387. if (key.substr(0, 1) === '_') {
  1388. return 'skip';
  1389. }
  1390. if (value === null) {
  1391. return 'null';
  1392. }
  1393. if (typeof value === 'number') {
  1394. return 'number';
  1395. }
  1396. if (typeof value === 'boolean') {
  1397. return 'checkbox';
  1398. }
  1399. if (typeof value === 'object') {
  1400. return 'skip';
  1401. }
  1402. return 'text';
  1403. };
  1404. $scope.themeName = function (theme) {
  1405. return theme.replace('-', ' ').replace(/(?:^|\s)\S/g, function (a) {
  1406. return a.toUpperCase();
  1407. });
  1408. };
  1409. // pseudo main. called on all definitions assigned
  1410. initController();
  1411. });