syncthingController.js 113 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888
  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, $q, $compile, $timeout, $rootScope, $translate) {
  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.discoveryCache = {};
  27. $scope.protocolChanged = false;
  28. $scope.reportData = {};
  29. $scope.reportDataPreview = '';
  30. $scope.reportPreview = false;
  31. $scope.folders = {};
  32. $scope.seenError = '';
  33. $scope.upgradeInfo = null;
  34. $scope.deviceStats = {};
  35. $scope.folderStats = {};
  36. $scope.pendingDevices = {};
  37. $scope.pendingFolders = {};
  38. $scope.progress = {};
  39. $scope.version = {};
  40. $scope.needed = {};
  41. $scope.neededFolder = '';
  42. $scope.failed = {};
  43. $scope.localChanged = {};
  44. $scope.scanProgress = {};
  45. $scope.themes = [];
  46. $scope.globalChangeEvents = {};
  47. $scope.metricRates = false;
  48. $scope.folderPathErrors = {};
  49. $scope.currentSharing = {};
  50. $scope.currentFolder = {};
  51. $scope.currentDevice = {};
  52. $scope.ignores = {
  53. text: '',
  54. error: null,
  55. disabled: false,
  56. };
  57. resetRemoteNeed();
  58. try {
  59. $scope.metricRates = (window.localStorage["metricRates"] == "true");
  60. } catch (exception) { }
  61. $scope.versioningDefaults = {
  62. selector: "none",
  63. trashcanClean: 0,
  64. cleanupIntervalS: 3600,
  65. simpleKeep: 5,
  66. staggeredMaxAge: 365,
  67. staggeredCleanInterval: 3600,
  68. externalCommand: "",
  69. };
  70. $scope.localStateTotal = {
  71. bytes: 0,
  72. directories: 0,
  73. files: 0
  74. };
  75. $(window).bind('beforeunload', function () {
  76. navigatingAway = true;
  77. });
  78. $scope.$on("$locationChangeSuccess", function () {
  79. LocaleService.useLocale($location.search().lang);
  80. });
  81. $scope.needActions = {
  82. 'rm': 'Del',
  83. 'rmdir': 'Del (dir)',
  84. 'sync': 'Sync',
  85. 'touch': 'Update'
  86. };
  87. $scope.needIcons = {
  88. 'rm': 'far fa-fw fa-trash-alt',
  89. 'rmdir': 'far fa-fw fa-trash-alt',
  90. 'sync': 'far fa-fw fa-arrow-alt-circle-down',
  91. 'touch': 'fas fa-fw fa-asterisk'
  92. };
  93. $scope.$on(Events.ONLINE, function () {
  94. if (online && !restarting) {
  95. return;
  96. }
  97. console.log('UIOnline');
  98. refreshDeviceStats();
  99. refreshFolderStats();
  100. refreshGlobalChanges();
  101. refreshThemes();
  102. $q.all([
  103. refreshSystem(),
  104. refreshDiscoveryCache(),
  105. refreshConfig(),
  106. refreshCluster(),
  107. refreshConnectionStats(),
  108. ]).then(function() {
  109. $http.get(urlbase + '/system/version').success(function (data) {
  110. console.log("version", data);
  111. if ($scope.version.version && $scope.version.version !== data.version) {
  112. // We already have a version response, but it differs from
  113. // the new one. Reload the full GUI in case it's changed.
  114. document.location.reload(true);
  115. }
  116. $scope.version = data;
  117. }).error($scope.emitHTTPError);
  118. $http.get(urlbase + '/svc/report').success(function (data) {
  119. $scope.reportData = data;
  120. if ($scope.system && $scope.config.options.urAccepted > -1 && $scope.config.options.urSeen < $scope.system.urVersionMax && $scope.config.options.urAccepted < $scope.system.urVersionMax) {
  121. // Usage reporting format has changed, prompt the user to re-accept.
  122. $('#ur').modal();
  123. }
  124. }).error($scope.emitHTTPError);
  125. $http.get(urlbase + '/system/upgrade').success(function (data) {
  126. $scope.upgradeInfo = data;
  127. }).error(function () {
  128. $scope.upgradeInfo = null;
  129. });
  130. online = true;
  131. restarting = false;
  132. $('#networkError').modal('hide');
  133. $('#restarting').modal('hide');
  134. $('#shutdown').modal('hide');
  135. }).catch($scope.emitHTTPError);
  136. });
  137. $scope.$on(Events.OFFLINE, function () {
  138. if (navigatingAway || !online) {
  139. return;
  140. }
  141. console.log('UIOffline');
  142. online = false;
  143. if (!restarting) {
  144. $('#networkError').modal();
  145. }
  146. });
  147. $scope.$on('HTTPError', function (event, arg) {
  148. // Emitted when a HTTP call fails. We use the status code to try
  149. // to figure out what's wrong.
  150. if (navigatingAway || !online) {
  151. return;
  152. }
  153. console.log('HTTPError', arg);
  154. online = false;
  155. if (!restarting) {
  156. if (arg.status === 0) {
  157. // A network error, not an HTTP error
  158. $scope.$emit(Events.OFFLINE);
  159. } else if (arg.status >= 400 && arg.status <= 599) {
  160. // A genuine HTTP error
  161. $('#networkError').modal('hide');
  162. $('#restarting').modal('hide');
  163. $('#shutdown').modal('hide');
  164. $('#httpError').modal();
  165. }
  166. }
  167. });
  168. $scope.$on(Events.STATE_CHANGED, function (event, arg) {
  169. var data = arg.data;
  170. if ($scope.model[data.folder]) {
  171. $scope.model[data.folder].state = data.to;
  172. $scope.model[data.folder].error = data.error;
  173. // If a folder has started scanning, then any scan progress is
  174. // also obsolete.
  175. if (data.to === 'scanning') {
  176. delete $scope.scanProgress[data.folder];
  177. }
  178. // If a folder finished scanning, then refresh folder stats
  179. // to update last scan time.
  180. if (data.from === 'scanning' && data.to === 'idle') {
  181. refreshFolderStats();
  182. }
  183. }
  184. });
  185. $scope.$on(Events.LOCAL_INDEX_UPDATED, function (event, arg) {
  186. refreshFolderStats();
  187. refreshGlobalChanges();
  188. });
  189. $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
  190. if (!$scope.connections[arg.data.id]) {
  191. return;
  192. }
  193. $scope.connections[arg.data.id].connected = false;
  194. refreshDeviceStats();
  195. });
  196. $scope.$on(Events.DEVICE_CONNECTED, function (event, arg) {
  197. if (!$scope.connections[arg.data.id]) {
  198. $scope.connections[arg.data.id] = {
  199. inbps: 0,
  200. outbps: 0,
  201. inBytesTotal: 0,
  202. outBytesTotal: 0,
  203. type: arg.data.type,
  204. address: arg.data.addr
  205. };
  206. $scope.completion[arg.data.id] = {
  207. _total: 100,
  208. _needBytes: 0,
  209. _needItems: 0
  210. };
  211. }
  212. });
  213. $scope.$on(Events.PENDING_DEVICES_CHANGED, function (event, arg) {
  214. if (!(arg.data.added || arg.data.removed)) {
  215. // Not enough information to update in place, just refresh it completely
  216. refreshCluster();
  217. return;
  218. }
  219. if (arg.data.added) {
  220. arg.data.added.forEach(function (rejected) {
  221. var pendingDevice = {
  222. time: arg.time,
  223. name: rejected.name,
  224. address: rejected.address
  225. };
  226. console.log("rejected device:", rejected.deviceID, pendingDevice);
  227. $scope.pendingDevices[rejected.deviceID] = pendingDevice;
  228. });
  229. }
  230. if (arg.data.removed) {
  231. arg.data.removed.forEach(function (dev) {
  232. console.log("no longer pending device:", dev.deviceID);
  233. delete $scope.pendingDevices[dev.deviceID];
  234. });
  235. }
  236. });
  237. $scope.$on(Events.PENDING_FOLDERS_CHANGED, function (event, arg) {
  238. if (!(arg.data.added || arg.data.removed)) {
  239. // Not enough information to update in place, just refresh it completely
  240. refreshCluster();
  241. return;
  242. }
  243. if (arg.data.added) {
  244. arg.data.added.forEach(function (rejected) {
  245. var offeringDevice = {
  246. time: arg.time,
  247. label: rejected.folderLabel,
  248. receiveEncrypted: rejected.receiveEncrypted,
  249. };
  250. console.log("rejected folder", rejected.folderID, "from device:", rejected.deviceID, offeringDevice);
  251. var pendingFolder = $scope.pendingFolders[rejected.folderID];
  252. if (pendingFolder === undefined) {
  253. pendingFolder = {
  254. offeredBy: {}
  255. };
  256. }
  257. pendingFolder.offeredBy[rejected.deviceID] = offeringDevice;
  258. $scope.pendingFolders[rejected.folderID] = pendingFolder;
  259. });
  260. }
  261. if (arg.data.removed) {
  262. arg.data.removed.forEach(function (folderDev) {
  263. console.log("no longer pending folder", folderDev.folderID, "from device:", folderDev.deviceID);
  264. if (folderDev.deviceID === undefined) {
  265. delete $scope.pendingFolders[folderDev.folderID];
  266. } else if ($scope.pendingFolders[folderDev.folderID]) {
  267. delete $scope.pendingFolders[folderDev.folderID].offeredBy[folderDev.deviceID];
  268. }
  269. });
  270. }
  271. });
  272. $scope.$on('ConfigLoaded', function () {
  273. if ($scope.config.options.urAccepted === 0) {
  274. // If usage reporting has been neither accepted nor declined,
  275. // we want to ask the user to make a choice. But we don't want
  276. // to bug them during initial setup, so we set a cookie with
  277. // the time of the first visit. When that cookie is present
  278. // and the time is more than four hours ago, we ask the
  279. // question.
  280. var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  281. if (!firstVisit) {
  282. document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
  283. } else {
  284. if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
  285. $('#ur').modal();
  286. }
  287. }
  288. }
  289. });
  290. $scope.$on(Events.CONFIG_SAVED, function (event, arg) {
  291. updateLocalConfig(arg.data);
  292. $http.get(urlbase + '/config/insync').success(function (data) {
  293. $scope.configInSync = data.configInSync;
  294. }).error($scope.emitHTTPError);
  295. });
  296. $scope.$on(Events.DOWNLOAD_PROGRESS, function (event, arg) {
  297. var stats = arg.data;
  298. var progress = {};
  299. for (var folder in stats) {
  300. progress[folder] = {};
  301. for (var file in stats[folder]) {
  302. var s = stats[folder][file];
  303. var reused = 100 * s.reused / s.total;
  304. var copiedFromOrigin = 100 * s.copiedFromOrigin / s.total;
  305. var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
  306. var pulled = 100 * s.pulled / s.total;
  307. var pulling = 100 * s.pulling / s.total;
  308. // We try to round up pulling to at least a percent so that it would be at least a bit visible.
  309. if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
  310. pulling = 1;
  311. }
  312. progress[folder][file] = {
  313. reused: reused,
  314. copiedFromOrigin: copiedFromOrigin,
  315. copiedFromElsewhere: copiedFromElsewhere,
  316. pulled: pulled,
  317. pulling: pulling,
  318. bytesTotal: s.bytesTotal,
  319. bytesDone: s.bytesDone,
  320. };
  321. }
  322. }
  323. for (var folder in $scope.progress) {
  324. if (!(folder in progress)) {
  325. if ($scope.neededFolder === folder) {
  326. $scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
  327. }
  328. } else if ($scope.neededFolder === folder) {
  329. for (file in $scope.progress[folder]) {
  330. if (!(file in progress[folder])) {
  331. $scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
  332. break;
  333. }
  334. }
  335. }
  336. }
  337. $scope.progress = progress;
  338. console.log("DownloadProgress", $scope.progress);
  339. });
  340. $scope.$on(Events.FOLDER_SUMMARY, function (event, arg) {
  341. var data = arg.data;
  342. $scope.model[data.folder] = data.summary;
  343. recalcLocalStateTotal();
  344. });
  345. $scope.$on(Events.FOLDER_COMPLETION, function (event, arg) {
  346. var data = arg.data;
  347. if (!$scope.completion[data.device]) {
  348. $scope.completion[data.device] = {};
  349. }
  350. $scope.completion[data.device][data.folder] = data;
  351. recalcCompletion(data.device);
  352. });
  353. $scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
  354. $scope.model[arg.data.folder].errors = arg.data.errors.length;
  355. });
  356. $scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
  357. var data = arg.data;
  358. $scope.scanProgress[data.folder] = {
  359. current: data.current,
  360. total: data.total,
  361. rate: data.rate
  362. };
  363. console.log("FolderScanProgress", data);
  364. });
  365. $scope.emitHTTPError = function (data, status, headers, config) {
  366. $scope.$emit('HTTPError', { data: data, status: status, headers: headers, config: config });
  367. };
  368. var debouncedFuncs = {};
  369. function refreshFolder(folder) {
  370. if ($scope.folders[folder].paused) {
  371. return;
  372. }
  373. var key = "refreshFolder" + folder;
  374. if (!debouncedFuncs[key]) {
  375. debouncedFuncs[key] = debounce(function () {
  376. $http.get(urlbase + '/db/status?folder=' + encodeURIComponent(folder)).success(function (data) {
  377. $scope.model[folder] = data;
  378. recalcLocalStateTotal();
  379. console.log("refreshFolder", folder, data);
  380. }).error($scope.emitHTTPError);
  381. }, 1000);
  382. }
  383. debouncedFuncs[key]();
  384. }
  385. function updateLocalConfig(config) {
  386. var hasConfig = !isEmptyObject($scope.config);
  387. $scope.config = config;
  388. $scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', ');
  389. $scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
  390. $scope.config.options._urAcceptedStr = "" + $scope.config.options.urAccepted;
  391. $scope.devices = deviceMap($scope.config.devices);
  392. for (var id in $scope.devices) {
  393. $scope.completion[id] = {
  394. _total: 100,
  395. _needBytes: 0,
  396. _needItems: 0
  397. };
  398. };
  399. $scope.folders = folderMap($scope.config.folders);
  400. Object.keys($scope.folders).forEach(function (folder) {
  401. refreshFolder(folder);
  402. $scope.folders[folder].devices.forEach(function (deviceCfg) {
  403. refreshCompletion(deviceCfg.deviceID, folder);
  404. });
  405. });
  406. refreshNoAuthWarning();
  407. setDefaultTheme();
  408. if (!hasConfig) {
  409. $scope.$emit('ConfigLoaded');
  410. }
  411. }
  412. function refreshSystem() {
  413. return $http.get(urlbase + '/system/status').success(function (data) {
  414. $scope.myID = data.myID;
  415. $scope.system = data;
  416. if ($scope.reportDataPreviewVersion === '') {
  417. $scope.reportDataPreviewVersion = $scope.system.urVersionMax;
  418. }
  419. var listenersFailed = [];
  420. var listenersRunning = [];
  421. for (var address in data.connectionServiceStatus) {
  422. if (data.connectionServiceStatus[address].error) {
  423. listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
  424. } else {
  425. listenersRunning.push(address);
  426. }
  427. }
  428. $scope.listenersFailed = listenersFailed;
  429. $scope.listenersRunning = listenersRunning;
  430. $scope.listenersTotal = $scope.sizeOf(data.connectionServiceStatus);
  431. var discoveryFailed = [];
  432. var discoveryRunning = [];
  433. for (var disco in data.discoveryStatus) {
  434. if (data.discoveryStatus[disco] && data.discoveryStatus[disco].error) {
  435. discoveryFailed.push(disco + ": " + data.discoveryStatus[disco].error);
  436. } else {
  437. discoveryRunning.push(disco);
  438. }
  439. }
  440. $scope.discoveryFailed = discoveryFailed;
  441. $scope.discoveryRunning = discoveryRunning;
  442. $scope.discoveryTotal = $scope.sizeOf(data.discoveryStatus);
  443. refreshNoAuthWarning();
  444. console.log("refreshSystem", data);
  445. }).error($scope.emitHTTPError);
  446. }
  447. function refreshNoAuthWarning() {
  448. if (!$scope.system || !$scope.config || !$scope.config.gui) {
  449. // We need all to be able to determine the state.
  450. return
  451. }
  452. // If we're not listening on localhost, and there is no
  453. // authentication configured, and the magic setting to silence the
  454. // warning isn't set, then yell at the user.
  455. var addr = $scope.system.guiAddressUsed;
  456. var guiCfg = $scope.config.gui;
  457. $scope.openNoAuth = addr.substr(0, 4) !== "127."
  458. && addr.substr(0, 6) !== "[::1]:"
  459. && addr.substr(0, 1) !== "/"
  460. && (!guiCfg.user || !guiCfg.password)
  461. && guiCfg.authMode !== 'ldap'
  462. && !guiCfg.insecureAdminAccess;
  463. if (guiCfg.user && guiCfg.password) {
  464. $scope.dismissNotification('authenticationUserAndPassword');
  465. }
  466. }
  467. function refreshCluster() {
  468. return $q.all([
  469. $http.get(urlbase + '/cluster/pending/devices').success(function (data) {
  470. $scope.pendingDevices = data;
  471. console.log("refreshCluster devices", data);
  472. }).error($scope.emitHTTPError),
  473. $http.get(urlbase + '/cluster/pending/folders').success(function (data) {
  474. $scope.pendingFolders = data;
  475. console.log("refreshCluster folders", data);
  476. }).error($scope.emitHTTPError),
  477. ]);
  478. }
  479. function refreshDiscoveryCache() {
  480. return $http.get(urlbase + '/system/discovery').success(function (data) {
  481. for (var device in data) {
  482. for (var i = 0; i < data[device].addresses.length; i++) {
  483. // Relay addresses are URLs with
  484. // .../?foo=barlongstuff that we strip away here. We
  485. // remove the final slash as well for symmetry with
  486. // tcp://192.0.2.42:1234 type addresses.
  487. data[device].addresses[i] = data[device].addresses[i].replace(/\/\?.*/, '');
  488. }
  489. }
  490. $scope.discoveryCache = data;
  491. console.log("refreshDiscoveryCache", data);
  492. }).error($scope.emitHTTPError);
  493. }
  494. function recalcLocalStateTotal() {
  495. $scope.localStateTotal = {
  496. bytes: 0,
  497. directories: 0,
  498. files: 0
  499. };
  500. for (var f in $scope.model) {
  501. $scope.localStateTotal.bytes += $scope.model[f].localBytes;
  502. $scope.localStateTotal.files += $scope.model[f].localFiles;
  503. $scope.localStateTotal.directories += $scope.model[f].localDirectories;
  504. }
  505. }
  506. function recalcCompletion(device) {
  507. var total = 0, needed = 0, deletes = 0, items = 0;
  508. for (var folder in $scope.completion[device]) {
  509. if (folder === "_total" || folder === '_needBytes' || folder === '_needItems') {
  510. continue;
  511. }
  512. total += $scope.completion[device][folder].globalBytes;
  513. needed += $scope.completion[device][folder].needBytes;
  514. items += $scope.completion[device][folder].needItems;
  515. deletes += $scope.completion[device][folder].needDeletes;
  516. }
  517. if (total == 0) {
  518. $scope.completion[device]._total = 100;
  519. $scope.completion[device]._needBytes = 0;
  520. $scope.completion[device]._needItems = 0;
  521. } else {
  522. $scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
  523. $scope.completion[device]._needBytes = needed;
  524. $scope.completion[device]._needItems = items + deletes;
  525. }
  526. if (needed == 0 && deletes > 0) {
  527. // We don't need any data, but we have deletes that we need
  528. // to do. Drop down the completion percentage to indicate
  529. // that we have stuff to do.
  530. $scope.completion[device]._total = 95;
  531. }
  532. console.log("recalcCompletion", device, $scope.completion[device]);
  533. }
  534. function refreshCompletion(device, folder) {
  535. if (device === $scope.myID) {
  536. return;
  537. }
  538. $http.get(urlbase + '/db/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) {
  539. if (!$scope.completion[device]) {
  540. $scope.completion[device] = {};
  541. }
  542. $scope.completion[device][folder] = data;
  543. recalcCompletion(device);
  544. }).error(function(data, status, headers, config) {
  545. if (status === 404) {
  546. console.log("refreshCompletion:", data);
  547. } else {
  548. $scope.emitHTTPError(data, status, headers, config);
  549. }
  550. });
  551. }
  552. function refreshConnectionStats() {
  553. return $http.get(urlbase + '/system/connections').success(function (data) {
  554. var now = Date.now(),
  555. td = (now - prevDate) / 1000,
  556. id;
  557. prevDate = now;
  558. try {
  559. data.total.inbps = Math.max(0, (data.total.inBytesTotal - $scope.connectionsTotal.inBytesTotal) / td);
  560. data.total.outbps = Math.max(0, (data.total.outBytesTotal - $scope.connectionsTotal.outBytesTotal) / td);
  561. } catch (e) {
  562. data.total.inbps = 0;
  563. data.total.outbps = 0;
  564. }
  565. $scope.connectionsTotal = data.total;
  566. data = data.connections;
  567. for (id in data) {
  568. if (!data.hasOwnProperty(id)) {
  569. continue;
  570. }
  571. try {
  572. data[id].inbps = Math.max(0, (data[id].inBytesTotal - $scope.connections[id].inBytesTotal) / td);
  573. data[id].outbps = Math.max(0, (data[id].outBytesTotal - $scope.connections[id].outBytesTotal) / td);
  574. } catch (e) {
  575. data[id].inbps = 0;
  576. data[id].outbps = 0;
  577. }
  578. }
  579. $scope.connections = data;
  580. console.log("refreshConnections", data);
  581. }).error($scope.emitHTTPError);
  582. }
  583. function refreshErrors() {
  584. $http.get(urlbase + '/system/error').success(function (data) {
  585. $scope.errors = data.errors;
  586. console.log("refreshErrors", data);
  587. }).error($scope.emitHTTPError);
  588. }
  589. function refreshConfig() {
  590. return $q.all([
  591. $http.get(urlbase + '/config').success(function (data) {
  592. updateLocalConfig(data);
  593. console.log("refreshConfig", data);
  594. }),
  595. $http.get(urlbase + '/config/insync').success(function (data) {
  596. $scope.configInSync = data.configInSync;
  597. }),
  598. ]);
  599. }
  600. $scope.refreshNeed = function (page, perpage) {
  601. if (!$scope.neededFolder) {
  602. return;
  603. }
  604. var url = urlbase + "/db/need?folder=" + encodeURIComponent($scope.neededFolder);
  605. url += "&page=" + page;
  606. url += "&perpage=" + perpage;
  607. $http.get(url).success(function (data) {
  608. console.log("refreshNeed", $scope.neededFolder, data);
  609. parseNeeded(data);
  610. }).error($scope.emitHTTPError);
  611. };
  612. function needAction(file) {
  613. var fDelete = 4096;
  614. var fDirectory = 16384;
  615. if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
  616. return 'rmdir';
  617. } else if ((file.flags & fDelete) === fDelete) {
  618. return 'rm';
  619. } else if ((file.flags & fDirectory) === fDirectory) {
  620. return 'touch';
  621. } else {
  622. return 'sync';
  623. }
  624. }
  625. function parseNeeded(data) {
  626. $scope.needed = data;
  627. var merged = [];
  628. data.progress.forEach(function (item) {
  629. item.type = "progress";
  630. item.action = needAction(item);
  631. merged.push(item);
  632. });
  633. data.queued.forEach(function (item) {
  634. item.type = "queued";
  635. item.action = needAction(item);
  636. merged.push(item);
  637. });
  638. data.rest.forEach(function (item) {
  639. item.type = "rest";
  640. item.action = needAction(item);
  641. merged.push(item);
  642. });
  643. $scope.needed.items = merged;
  644. }
  645. function pathJoin(base, name) {
  646. base = expandTilde(base);
  647. if (base[base.length - 1] !== $scope.system.pathSeparator) {
  648. return base + $scope.system.pathSeparator + name;
  649. }
  650. return base + name;
  651. }
  652. function expandTilde(path) {
  653. if (path && path.trim().charAt(0) === '~') {
  654. return $scope.system.tilde + path.trim().substring(1);
  655. }
  656. return path;
  657. }
  658. function shouldSetDefaultFolderPath() {
  659. return $scope.config.defaults.folder.path && !$scope.editingExisting && $scope.folderEditor.folderPath.$pristine && !$scope.editingDefaults;
  660. }
  661. function resetRemoteNeed() {
  662. $scope.remoteNeed = {};
  663. $scope.remoteNeedFolders = [];
  664. $scope.remoteNeedDevice = undefined;
  665. }
  666. function setDefaultTheme() {
  667. if (!document.getElementById("fallback-theme-css")) {
  668. // check if no support for prefers-color-scheme
  669. var colorSchemeNotSupported = typeof window.matchMedia === "undefined" || window.matchMedia('(prefers-color-scheme: dark)').media === 'not all';
  670. if ($scope.config.gui.theme === "default" && colorSchemeNotSupported) {
  671. document.documentElement.style.display = 'none';
  672. document.head.insertAdjacentHTML(
  673. 'beforeend',
  674. '<link id="fallback-theme-css" rel="stylesheet" href="theme-assets/light/assets/css/theme.css" onload="document.documentElement.style.display = \'\'">'
  675. );
  676. }
  677. }
  678. }
  679. function saveIgnores(ignores, cb) {
  680. $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
  681. ignore: ignores
  682. }).success(function () {
  683. if (cb) {
  684. cb();
  685. }
  686. });
  687. };
  688. function initShareEditing(editing) {
  689. $scope.currentSharing = {};
  690. $scope.currentSharing.editing = editing;
  691. $scope.currentSharing.shared = [];
  692. $scope.currentSharing.unrelated = [];
  693. $scope.currentSharing.selected = {};
  694. $scope.currentSharing.encryptionPasswords = {};
  695. if (editing === 'folder') {
  696. initShareEditingFolder();
  697. }
  698. };
  699. function initShareEditingFolder() {
  700. $scope.currentFolder.devices.forEach(function (n) {
  701. if (n.deviceID !== $scope.myID) {
  702. $scope.currentSharing.shared.push($scope.devices[n.deviceID]);
  703. }
  704. if (n.encryptionPassword !== '') {
  705. $scope.currentSharing.encryptionPasswords[n.deviceID] = n.encryptionPassword;
  706. }
  707. $scope.currentSharing.selected[n.deviceID] = true;
  708. });
  709. $scope.currentSharing.unrelated = $scope.deviceList().filter(function (n) {
  710. return n.deviceID !== $scope.myID && !$scope.currentSharing.selected[n.deviceID];
  711. });
  712. }
  713. $scope.pendingIsRemoteEncrypted = function(folderID, deviceID) {
  714. var pending = $scope.pendingFolders[folderID];
  715. if (!pending || !pending.offeredBy || !pending.offeredBy[deviceID]) {
  716. return false;
  717. }
  718. return pending.offeredBy[deviceID].remoteEncrypted;
  719. };
  720. $scope.refreshFailed = function (page, perpage) {
  721. if (!$scope.failed || !$scope.failed.folder) {
  722. return;
  723. }
  724. var url = urlbase + '/folder/errors?folder=' + encodeURIComponent($scope.failed.folder);
  725. url += "&page=" + page + "&perpage=" + perpage;
  726. $http.get(url).success(function (data) {
  727. $scope.failed = data;
  728. }).error($scope.emitHTTPError);
  729. };
  730. $scope.refreshRemoteNeed = function (folder, page, perpage) {
  731. if (!$scope.remoteNeedDevice) {
  732. return;
  733. }
  734. var url = urlbase + '/db/remoteneed?device=' + $scope.remoteNeedDevice.deviceID;
  735. url += '&folder=' + encodeURIComponent(folder);
  736. url += "&page=" + page + "&perpage=" + perpage;
  737. $http.get(url).success(function (data) {
  738. $scope.remoteNeed[folder] = data;
  739. }).error(function (err) {
  740. $scope.remoteNeed[folder] = undefined;
  741. $scope.emitHTTPError(err);
  742. });
  743. };
  744. $scope.refreshLocalChanged = function (page, perpage) {
  745. if (!$scope.localChangedFolder) {
  746. return;
  747. }
  748. var url = urlbase + '/db/localchanged?folder=';
  749. url += encodeURIComponent($scope.localChangedFolder);
  750. url += "&page=" + page + "&perpage=" + perpage;
  751. $http.get(url).success(function (data) {
  752. $scope.localChanged = data;
  753. }).error($scope.emitHTTPError);
  754. };
  755. var refreshDeviceStats = debounce(function () {
  756. $http.get(urlbase + "/stats/device").success(function (data) {
  757. $scope.deviceStats = data;
  758. for (var device in $scope.deviceStats) {
  759. $scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
  760. $scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
  761. }
  762. console.log("refreshDeviceStats", data);
  763. }).error($scope.emitHTTPError);
  764. }, 2500);
  765. var refreshFolderStats = debounce(function () {
  766. $http.get(urlbase + "/stats/folder").success(function (data) {
  767. $scope.folderStats = data;
  768. for (var folder in $scope.folderStats) {
  769. if ($scope.folderStats[folder].lastFile) {
  770. $scope.folderStats[folder].lastFile.at = new Date($scope.folderStats[folder].lastFile.at);
  771. }
  772. $scope.folderStats[folder].lastScan = new Date($scope.folderStats[folder].lastScan);
  773. $scope.folderStats[folder].lastScanDays = (new Date() - $scope.folderStats[folder].lastScan) / 1000 / 86400;
  774. }
  775. console.log("refreshfolderStats", data);
  776. }).error($scope.emitHTTPError);
  777. }, 2500);
  778. var refreshThemes = debounce(function () {
  779. $http.get("themes.json").success(function (data) { // no urlbase here as this is served by the asset handler
  780. $scope.themes = data.themes;
  781. }).error($scope.emitHTTPError);
  782. }, 2500);
  783. var refreshGlobalChanges = debounce(function () {
  784. $http.get(urlbase + "/events/disk?limit=25").success(function (data) {
  785. if (!data) {
  786. // For reasons unknown this is called with data being the empty
  787. // string on shutdown, causing an error on .reverse().
  788. return;
  789. }
  790. data = data.reverse();
  791. $scope.globalChangeEvents = data;
  792. console.log("refreshGlobalChanges", data);
  793. }).error($scope.emitHTTPError);
  794. }, 2500);
  795. $scope.refresh = function () {
  796. refreshSystem();
  797. refreshDiscoveryCache();
  798. refreshConnectionStats();
  799. refreshErrors();
  800. };
  801. $scope.folderStatus = function (folderCfg) {
  802. if (folderCfg.paused) {
  803. return 'paused';
  804. }
  805. var folderInfo = $scope.model[folderCfg.id];
  806. // after restart syncthing process state may be empty
  807. if (typeof folderInfo === 'undefined' || !folderInfo.state) {
  808. return 'unknown';
  809. }
  810. var state = '' + folderInfo.state;
  811. if (state === 'error') {
  812. return 'stopped'; // legacy, the state is called "stopped" in the GUI
  813. }
  814. if (state !== 'idle') {
  815. return state;
  816. }
  817. if (folderInfo.needTotalItems > 0) {
  818. return 'outofsync';
  819. }
  820. if ($scope.hasFailedFiles(folderCfg.id)) {
  821. return 'faileditems';
  822. }
  823. if ($scope.hasReceiveOnlyChanged(folderCfg)) {
  824. return 'localadditions';
  825. }
  826. if ($scope.hasReceiveEncryptedItems(folderCfg)) {
  827. return 'localunencrypted';
  828. }
  829. if (folderCfg.devices.length <= 1) {
  830. return 'unshared';
  831. }
  832. return state;
  833. };
  834. $scope.folderClass = function (folderCfg) {
  835. var status = $scope.folderStatus(folderCfg);
  836. if (status === 'idle' || status === 'localadditions') {
  837. return 'success';
  838. }
  839. if (status == 'paused') {
  840. return 'default';
  841. }
  842. if (status === 'syncing' || status === 'sync-preparing' || status === 'scanning' || status === 'cleaning') {
  843. return 'primary';
  844. }
  845. if (status === 'unknown') {
  846. return 'info';
  847. }
  848. if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems' || status === 'localunencrypted') {
  849. return 'danger';
  850. }
  851. if (status === 'unshared' || status === 'scan-waiting' || status === 'sync-waiting' || status === 'clean-waiting') {
  852. return 'warning';
  853. }
  854. return 'info';
  855. };
  856. $scope.syncPercentage = function (folder) {
  857. if (typeof $scope.model[folder] === 'undefined') {
  858. return 100;
  859. }
  860. if ($scope.model[folder].needTotalItems === 0) {
  861. return 100;
  862. }
  863. if (($scope.model[folder].needBytes == 0 && $scope.model[folder].needDeletes > 0) || $scope.model[folder].globalBytes == 0) {
  864. // We don't need any data, but we have deletes that we need
  865. // to do. Drop down the completion percentage to indicate
  866. // that we have stuff to do.
  867. // Do the same thing in case we only have zero byte files to sync.
  868. return 95;
  869. }
  870. var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes;
  871. return Math.floor(pct);
  872. };
  873. $scope.scanPercentage = function (folder) {
  874. if (!$scope.scanProgress[folder]) {
  875. return undefined;
  876. }
  877. var pct = 100 * $scope.scanProgress[folder].current / $scope.scanProgress[folder].total;
  878. return Math.floor(pct);
  879. };
  880. $scope.scanRate = function (folder) {
  881. if (!$scope.scanProgress[folder]) {
  882. return 0;
  883. }
  884. return $scope.scanProgress[folder].rate;
  885. };
  886. $scope.scanRemaining = function (folder) {
  887. // Formats the remaining scan time as a string. Includes days and
  888. // hours only when relevant, resulting in time stamps like:
  889. // 00m 40s
  890. // 32m 40s
  891. // 2h 32m
  892. // 4d 2h
  893. // In case remaining scan time appears to be >31d, omit the
  894. // details, i.e.:
  895. // > 1 month
  896. if (!$scope.scanProgress[folder]) {
  897. return "";
  898. }
  899. // Calculate remaining bytes and seconds based on our current
  900. // rate.
  901. var remainingBytes = $scope.scanProgress[folder].total - $scope.scanProgress[folder].current;
  902. var seconds = remainingBytes / $scope.scanProgress[folder].rate;
  903. // Round up to closest ten seconds to avoid flapping too much to
  904. // and fro.
  905. seconds = Math.ceil(seconds / 10) * 10;
  906. // Separate out the number of days.
  907. var days = 0;
  908. var res = [];
  909. if (seconds >= 86400) {
  910. days = Math.floor(seconds / 86400);
  911. if (days > 31) {
  912. return '> 1 month';
  913. }
  914. res.push('' + days + 'd');
  915. seconds = seconds % 86400;
  916. }
  917. // Separate out the number of hours.
  918. var hours = 0;
  919. if (seconds > 3600) {
  920. hours = Math.floor(seconds / 3600);
  921. res.push('' + hours + 'h');
  922. seconds = seconds % 3600;
  923. }
  924. var d = new Date(1970, 0, 1).setSeconds(seconds);
  925. if (days === 0) {
  926. // Format minutes only if we're within a day of completion.
  927. var f = $filter('date')(d, "m'm'");
  928. res.push(f);
  929. }
  930. if (days === 0 && hours === 0) {
  931. // Format seconds only when we're within an hour of completion.
  932. var f = $filter('date')(d, "ss's'");
  933. res.push(f);
  934. }
  935. return res.join(' ');
  936. };
  937. $scope.deviceStatus = function (deviceCfg) {
  938. var status = '';
  939. if ($scope.deviceFolders(deviceCfg).length === 0) {
  940. status = 'unused-';
  941. }
  942. if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
  943. return 'unknown';
  944. }
  945. if (deviceCfg.paused) {
  946. return status + 'paused';
  947. }
  948. if ($scope.connections[deviceCfg.deviceID].connected) {
  949. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  950. return status + 'insync';
  951. } else {
  952. return 'syncing';
  953. }
  954. }
  955. // Disconnected
  956. return status + 'disconnected';
  957. };
  958. $scope.deviceClass = function (deviceCfg) {
  959. if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
  960. return 'info';
  961. }
  962. if (deviceCfg.paused) {
  963. return 'default';
  964. }
  965. if ($scope.connections[deviceCfg.deviceID].connected) {
  966. if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
  967. return 'success';
  968. } else {
  969. return 'primary';
  970. }
  971. }
  972. // Disconnected
  973. return 'info';
  974. };
  975. $scope.syncthingStatus = function () {
  976. var syncCount = 0;
  977. var notifyCount = 0;
  978. var pauseCount = 0;
  979. // loop through all folders
  980. var folderListCache = $scope.folderList();
  981. for (var i = 0; i < folderListCache.length; i++) {
  982. var status = $scope.folderStatus(folderListCache[i]);
  983. switch (status) {
  984. case 'sync-preparing':
  985. case 'syncing':
  986. syncCount++;
  987. break;
  988. case 'stopped':
  989. case 'unknown':
  990. case 'outofsync':
  991. case 'error':
  992. notifyCount++;
  993. break;
  994. }
  995. }
  996. // loop through all devices
  997. var deviceCount = 0;
  998. for (var id in $scope.devices) {
  999. var status = $scope.deviceStatus({
  1000. deviceID: id
  1001. });
  1002. switch (status) {
  1003. case 'unknown':
  1004. notifyCount++;
  1005. break;
  1006. case 'paused':
  1007. pauseCount++;
  1008. break;
  1009. case 'unused':
  1010. deviceCount--;
  1011. break;
  1012. }
  1013. deviceCount++;
  1014. }
  1015. // enumerate notifications
  1016. if ($scope.openNoAuth || !$scope.configInSync || $scope.errorList().length > 0 || !online || Object.keys($scope.pendingDevices).length > 0 || Object.keys($scope.pendingFolders).length > 0) {
  1017. notifyCount++;
  1018. }
  1019. // at least one folder is syncing
  1020. if (syncCount > 0) {
  1021. return 'sync';
  1022. }
  1023. // a device is unknown or a folder is stopped/unknown/outofsync/error or some other notification is open or gui offline
  1024. if (notifyCount > 0) {
  1025. return 'notify';
  1026. }
  1027. // all used devices are paused except (this) one
  1028. if (pauseCount === deviceCount - 1) {
  1029. return 'pause';
  1030. }
  1031. return 'default';
  1032. };
  1033. $scope.deviceAddr = function (deviceCfg) {
  1034. var conn = $scope.connections[deviceCfg.deviceID];
  1035. if (conn && conn.connected) {
  1036. return conn.address;
  1037. }
  1038. return '?';
  1039. };
  1040. $scope.hasRemoteGUIAddress = function (deviceCfg) {
  1041. if (!deviceCfg.remoteGUIPort)
  1042. return false;
  1043. var conn = $scope.connections[deviceCfg.deviceID];
  1044. return conn && conn.connected && conn.address && conn.type.indexOf('Relay') == -1;
  1045. };
  1046. $scope.remoteGUIAddress = function (deviceCfg) {
  1047. // Assume hasRemoteGUIAddress is true or we would not be here
  1048. var conn = $scope.connections[deviceCfg.deviceID];
  1049. return 'http://' + replaceAddressPort(conn.address, deviceCfg.remoteGUIPort);
  1050. };
  1051. function replaceAddressPort(address, newPort) {
  1052. for (var index = address.length - 1; index >= 0; index--) {
  1053. if (address[index] === ":") {
  1054. return address.substr(0, index) + ":" + newPort.toString();
  1055. }
  1056. }
  1057. return address;
  1058. }
  1059. $scope.friendlyNameFromShort = function (shortID) {
  1060. var matches = Object.keys($scope.devices).filter(function (id) {
  1061. return id.substr(0, 7) === shortID;
  1062. });
  1063. if (matches.length !== 1) {
  1064. return shortID;
  1065. }
  1066. return $scope.friendlyNameFromID(matches[0]);
  1067. };
  1068. $scope.friendlyNameFromID = function (deviceID) {
  1069. var match = $scope.devices[deviceID];
  1070. if (match) {
  1071. return $scope.deviceName(match);
  1072. }
  1073. return deviceID.substr(0, 6);
  1074. };
  1075. $scope.deviceName = function (deviceCfg) {
  1076. if (typeof deviceCfg === 'undefined') {
  1077. return "";
  1078. }
  1079. if (deviceCfg.name) {
  1080. return deviceCfg.name;
  1081. }
  1082. return $scope.deviceShortID(deviceCfg.deviceID);
  1083. };
  1084. $scope.deviceShortID = function (deviceID) {
  1085. if (typeof deviceID === 'undefined') {
  1086. return "";
  1087. }
  1088. return deviceID.substr(0, 6);
  1089. };
  1090. $scope.thisDeviceName = function () {
  1091. var device = $scope.thisDevice();
  1092. if (typeof device === 'undefined') {
  1093. return "(unknown device)";
  1094. }
  1095. if (device.name) {
  1096. return device.name;
  1097. }
  1098. return device.deviceID.substr(0, 6);
  1099. };
  1100. $scope.showDeviceIdentification = function (deviceCfg) {
  1101. $scope.currentDevice = deviceCfg;
  1102. $('#idqr').modal();
  1103. };
  1104. $scope.setDevicePause = function (device, pause) {
  1105. $scope.devices[device].paused = pause;
  1106. $scope.config.devices = $scope.deviceList();
  1107. $scope.saveConfig();
  1108. };
  1109. $scope.setFolderPause = function (folder, pause) {
  1110. var cfg = $scope.folders[folder];
  1111. if (cfg) {
  1112. cfg.paused = pause;
  1113. $scope.config.folders = folderList($scope.folders);
  1114. $scope.saveConfig();
  1115. }
  1116. };
  1117. $scope.showListenerStatus = function () {
  1118. var params = {
  1119. type: 'listeners',
  1120. };
  1121. if ($scope.listenersFailed.length > 0) {
  1122. params.status = 'danger';
  1123. params.heading = $translate.instant("Listener Failures");
  1124. } else {
  1125. params.status = 'default';
  1126. params.heading = $translate.instant("Listener Status");
  1127. }
  1128. $scope.connectivityStatusParams = params;
  1129. $('#connectivity-status').modal();
  1130. };
  1131. $scope.showDiscoveryStatus = function () {
  1132. var params = {
  1133. type: 'discovery',
  1134. };
  1135. if ($scope.discoveryFailed.length > 0) {
  1136. params.status = 'danger';
  1137. params.heading = $translate.instant("Discovery Failures");
  1138. } else {
  1139. params.status = 'default';
  1140. params.heading = $translate.instant("Discovery Status");
  1141. }
  1142. $scope.connectivityStatusParams = params;
  1143. $('#connectivity-status').modal();
  1144. };
  1145. $scope.logging = {
  1146. facilities: {},
  1147. refreshFacilities: function () {
  1148. $http.get(urlbase + '/system/debug').success(function (data) {
  1149. var facilities = {};
  1150. data.enabled = data.enabled || [];
  1151. $.each(data.facilities, function (key, value) {
  1152. facilities[key] = {
  1153. description: value,
  1154. enabled: data.enabled.indexOf(key) > -1
  1155. }
  1156. })
  1157. $scope.logging.facilities = facilities;
  1158. }).error($scope.emitHTTPError);
  1159. },
  1160. show: function () {
  1161. $scope.logging.paused = false;
  1162. $scope.logging.refreshFacilities();
  1163. $scope.logging.timer = $timeout($scope.logging.fetch);
  1164. var textArea = $('#logViewerText');
  1165. textArea.on("scroll", $scope.logging.onScroll);
  1166. $('#logViewer').modal().one('shown.bs.modal', function () {
  1167. // Scroll to bottom.
  1168. textArea.scrollTop(textArea[0].scrollHeight);
  1169. }).one('hidden.bs.modal', function () {
  1170. $timeout.cancel($scope.logging.timer);
  1171. textArea.off("scroll", $scope.logging.onScroll);
  1172. $scope.logging.timer = null;
  1173. $scope.logging.entries = [];
  1174. });
  1175. },
  1176. onFacilityChange: function (facility) {
  1177. var enabled = $scope.logging.facilities[facility].enabled;
  1178. // Disable checkboxes while we're in flight.
  1179. $.each($scope.logging.facilities, function (key) {
  1180. $scope.logging.facilities[key].enabled = null;
  1181. })
  1182. $http.post(urlbase + '/system/debug?' + (enabled ? 'enable=' : 'disable=') + facility)
  1183. .success($scope.logging.refreshFacilities)
  1184. .error($scope.emitHTTPError);
  1185. },
  1186. onScroll: function () {
  1187. var textArea = $('#logViewerText');
  1188. var scrollTop = textArea.prop('scrollTop');
  1189. var scrollHeight = textArea.prop('scrollHeight');
  1190. $scope.logging.paused = scrollHeight > (scrollTop + textArea.outerHeight());
  1191. // Browser events do not cause redraw, trigger manually.
  1192. $scope.$apply();
  1193. },
  1194. timer: null,
  1195. entries: [],
  1196. paused: false,
  1197. content: function () {
  1198. var content = "";
  1199. $.each($scope.logging.entries, function (idx, entry) {
  1200. content += entry.when.split('.')[0].replace('T', ' ') + ' ' + entry.message + "\n";
  1201. });
  1202. return content;
  1203. },
  1204. fetch: function () {
  1205. var textArea = $('#logViewerText');
  1206. if ($scope.logging.paused) {
  1207. if (!$scope.logging.timer) return;
  1208. $scope.logging.timer = $timeout($scope.logging.fetch, 500);
  1209. return;
  1210. }
  1211. var last = null;
  1212. if ($scope.logging.entries.length > 0) {
  1213. last = $scope.logging.entries[$scope.logging.entries.length - 1].when;
  1214. }
  1215. $http.get(urlbase + '/system/log' + (last ? '?since=' + encodeURIComponent(last) : '')).success(function (data) {
  1216. if (!$scope.logging.timer) return;
  1217. $scope.logging.timer = $timeout($scope.logging.fetch, 2000);
  1218. if (!$scope.logging.paused) {
  1219. if (data.messages) {
  1220. $scope.logging.entries.push.apply($scope.logging.entries, data.messages);
  1221. // Wait for the text area to be redrawn, adding new lines, and then scroll to bottom.
  1222. $timeout(function () {
  1223. textArea.scrollTop(textArea[0].scrollHeight);
  1224. });
  1225. }
  1226. }
  1227. });
  1228. }
  1229. };
  1230. $scope.discardChangedSettings = function () {
  1231. $("#discard-changes-confirmation").modal("hide");
  1232. $("#settings").off("hide.bs.modal").modal("hide");
  1233. };
  1234. $scope.showSettings = function () {
  1235. // Make a working copy
  1236. $scope.tmpOptions = angular.copy($scope.config.options);
  1237. $scope.tmpOptions.deviceName = $scope.thisDevice().name;
  1238. $scope.tmpOptions.upgrades = "none";
  1239. if ($scope.tmpOptions.autoUpgradeIntervalH > 0) {
  1240. $scope.tmpOptions.upgrades = "stable";
  1241. }
  1242. if ($scope.tmpOptions.upgradeToPreReleases) {
  1243. $scope.tmpOptions.upgrades = "candidate";
  1244. }
  1245. $scope.tmpGUI = angular.copy($scope.config.gui);
  1246. $scope.tmpRemoteIgnoredDevices = angular.copy($scope.config.remoteIgnoredDevices);
  1247. $scope.tmpDevices = angular.copy($scope.config.devices);
  1248. $('#settings').modal("show");
  1249. $("#settings a[href='#settings-general']").tab("show");
  1250. $("#settings").on('hide.bs.modal', function (event) {
  1251. if ($scope.settingsModified()) {
  1252. event.preventDefault();
  1253. $("#discard-changes-confirmation").modal("show");
  1254. } else {
  1255. $("#settings").off("hide.bs.modal");
  1256. }
  1257. });
  1258. };
  1259. $scope.saveConfig = function (callback) {
  1260. var cfg = JSON.stringify($scope.config);
  1261. var opts = {
  1262. headers: {
  1263. 'Content-Type': 'application/json'
  1264. }
  1265. };
  1266. $http.put(urlbase + '/config', cfg, opts).finally(refreshConfig).then(function() {
  1267. if (callback) {
  1268. callback();
  1269. }
  1270. }, $scope.emitHTTPError);
  1271. };
  1272. $scope.urVersions = function () {
  1273. var result = [];
  1274. if ($scope.system) {
  1275. for (var i = $scope.system.urVersionMax; i >= 2; i--) {
  1276. result.push("" + i);
  1277. }
  1278. }
  1279. return result;
  1280. };
  1281. $scope.settingsModified = function () {
  1282. // Options has artificial properties injected into the temp config.
  1283. // Need to recompute them before we can check equality
  1284. var options = angular.copy($scope.config.options);
  1285. options.deviceName = $scope.thisDevice().name;
  1286. options.upgrades = "none";
  1287. if (options.autoUpgradeIntervalH > 0) {
  1288. options.upgrades = "stable";
  1289. }
  1290. if (options.upgradeToPreReleases) {
  1291. options.upgrades = "candidate";
  1292. }
  1293. var optionsEqual = angular.equals(options, $scope.tmpOptions);
  1294. var guiEquals = angular.equals($scope.config.gui, $scope.tmpGUI);
  1295. var ignoredDevicesEquals = angular.equals($scope.config.remoteIgnoredDevices, $scope.tmpRemoteIgnoredDevices);
  1296. var ignoredFoldersEquals = angular.equals($scope.config.devices, $scope.tmpDevices);
  1297. console.log("settings equals - options: " + optionsEqual + " gui: " + guiEquals + " ignDev: " + ignoredDevicesEquals + " ignFol: " + ignoredFoldersEquals);
  1298. return !optionsEqual || !guiEquals || !ignoredDevicesEquals || !ignoredFoldersEquals;
  1299. };
  1300. $scope.saveSettings = function () {
  1301. // Make sure something changed
  1302. if ($scope.settingsModified()) {
  1303. var themeChanged = $scope.config.gui.theme !== $scope.tmpGUI.theme;
  1304. // Angular has issues with selects with numeric values, so we handle strings here.
  1305. $scope.tmpOptions.urAccepted = parseInt($scope.tmpOptions._urAcceptedStr);
  1306. // Check if auto-upgrade has been enabled or disabled. This
  1307. // also has an effect on usage reporting, so do the check
  1308. // for that later.
  1309. if ($scope.tmpOptions.upgrades == "candidate") {
  1310. $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
  1311. $scope.tmpOptions.upgradeToPreReleases = true;
  1312. $scope.tmpOptions.urAccepted = $scope.system.urVersionMax;
  1313. $scope.tmpOptions.urSeen = $scope.system.urVersionMax;
  1314. } else if ($scope.tmpOptions.upgrades == "stable") {
  1315. $scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
  1316. $scope.tmpOptions.upgradeToPreReleases = false;
  1317. } else {
  1318. $scope.tmpOptions.autoUpgradeIntervalH = 0;
  1319. $scope.tmpOptions.upgradeToPreReleases = false;
  1320. }
  1321. // Check if protocol will need to be changed on restart
  1322. if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) {
  1323. $scope.protocolChanged = true;
  1324. }
  1325. // Parse strings to arrays before copying over
  1326. ['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
  1327. $scope.tmpOptions[key] = $scope.tmpOptions["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
  1328. return x.trim();
  1329. });
  1330. });
  1331. // Apply new settings locally
  1332. $scope.thisDeviceIn($scope.tmpDevices).name = $scope.tmpOptions.deviceName;
  1333. $scope.config.options = angular.copy($scope.tmpOptions);
  1334. $scope.config.gui = angular.copy($scope.tmpGUI);
  1335. $scope.config.remoteIgnoredDevices = angular.copy($scope.tmpRemoteIgnoredDevices);
  1336. $scope.config.devices = angular.copy($scope.tmpDevices);
  1337. // $scope.devices is updated by updateLocalConfig based on
  1338. // the config changed event, but settingsModified will look
  1339. // at it before that and conclude that the settings are
  1340. // modified (even though we just saved) unless we update
  1341. // here as well...
  1342. $scope.devices = deviceMap($scope.config.devices);
  1343. $scope.saveConfig(function () {
  1344. if (themeChanged) {
  1345. document.location.reload(true);
  1346. }
  1347. });
  1348. }
  1349. $("#settings").off("hide.bs.modal").modal("hide");
  1350. };
  1351. $scope.saveAdvanced = function () {
  1352. $scope.config = $scope.advancedConfig;
  1353. $scope.saveConfig();
  1354. $('#advanced').modal("hide");
  1355. };
  1356. $scope.restart = function () {
  1357. restarting = true;
  1358. $('#restarting').modal();
  1359. $http.post(urlbase + '/system/restart');
  1360. $scope.configInSync = true;
  1361. // Switch webpage protocol if needed
  1362. if ($scope.protocolChanged) {
  1363. var protocol = 'http';
  1364. if ($scope.config.gui.useTLS) {
  1365. protocol = 'https';
  1366. }
  1367. setTimeout(function () {
  1368. window.location.protocol = protocol;
  1369. }, 2500);
  1370. $scope.protocolChanged = false;
  1371. }
  1372. };
  1373. $scope.upgrade = function () {
  1374. restarting = true;
  1375. $('#upgrade').modal('hide');
  1376. $('#majorUpgrade').modal('hide');
  1377. $('#upgrading').modal();
  1378. $http.post(urlbase + '/system/upgrade').success(function () {
  1379. $('#restarting').modal();
  1380. $('#upgrading').modal('hide');
  1381. }).error(function () {
  1382. $('#upgrading').modal('hide');
  1383. });
  1384. };
  1385. $scope.shutdown = function () {
  1386. restarting = true;
  1387. $http.post(urlbase + '/system/shutdown').success(function () {
  1388. $('#shutdown').modal();
  1389. }).error($scope.emitHTTPError);
  1390. $scope.configInSync = true;
  1391. };
  1392. function editDeviceModal() {
  1393. $scope.currentDevice._addressesStr = $scope.currentDevice.addresses.join(', ');
  1394. $scope.deviceEditor.$setPristine();
  1395. $('#editDevice').modal();
  1396. }
  1397. $scope.editDeviceModalTitle = function() {
  1398. if ($scope.editingDefaults) {
  1399. return $translate.instant("Edit Device Defaults");
  1400. }
  1401. var title = '';
  1402. if ($scope.editingExisting) {
  1403. title += $translate.instant("Edit Device");
  1404. } else {
  1405. title += $translate.instant("Add Device");
  1406. }
  1407. var name = $scope.deviceName($scope.currentDevice);
  1408. if (name !== '') {
  1409. title += ' (' + name + ')';
  1410. }
  1411. return title;
  1412. };
  1413. $scope.editDeviceModalIcon = function() {
  1414. if ($scope.editingDefaults || $scope.editingExisting) {
  1415. return 'fas fa-pencil-alt';
  1416. }
  1417. return 'fas fa-desktop';
  1418. };
  1419. $scope.editDeviceExisting = function (deviceCfg) {
  1420. $scope.currentDevice = $.extend({}, deviceCfg);
  1421. $scope.editingExisting = true;
  1422. $scope.editingDefaults = false;
  1423. $scope.willBeReintroducedBy = undefined;
  1424. if (deviceCfg.introducedBy) {
  1425. var introducerDevice = $scope.devices[deviceCfg.introducedBy];
  1426. if (introducerDevice && introducerDevice.introducer) {
  1427. $scope.willBeReintroducedBy = $scope.deviceName(introducerDevice);
  1428. }
  1429. }
  1430. initShareEditing('device');
  1431. $scope.deviceFolders($scope.currentDevice).forEach(function (folderID) {
  1432. $scope.currentSharing.shared.push($scope.folders[folderID]);
  1433. $scope.currentSharing.selected[folderID] = true;
  1434. var folderdevices = $scope.folders[folderID].devices;
  1435. for (var i = 0; i < folderdevices.length; i++) {
  1436. if (folderdevices[i].deviceID === deviceCfg.deviceID) {
  1437. $scope.currentSharing.encryptionPasswords[folderID] = folderdevices[i].encryptionPassword;
  1438. break;
  1439. }
  1440. }
  1441. });
  1442. $scope.currentSharing.unrelated = $scope.folderList().filter(function (n) {
  1443. return !$scope.currentSharing.selected[n.id];
  1444. });
  1445. editDeviceModal();
  1446. };
  1447. $scope.editDeviceDefaults = function () {
  1448. $http.get(urlbase + '/config/defaults/device').then(function (p) {
  1449. $scope.currentDevice = p.data;
  1450. $scope.editingDefaults = true;
  1451. editDeviceModal();
  1452. }, $scope.emitHTTPError);
  1453. };
  1454. $scope.selectAllSharedFolders = function (state) {
  1455. var folders = $scope.currentSharing.shared;
  1456. for (var i = 0; i < folders.length; i++) {
  1457. $scope.currentSharing.selected[folders[i].id] = !!state;
  1458. }
  1459. };
  1460. $scope.selectAllUnrelatedFolders = function (state) {
  1461. var folders = $scope.currentSharing.unrelated;
  1462. for (var i = 0; i < folders.length; i++) {
  1463. $scope.currentSharing.selected[folders[i].id] = !!state;
  1464. }
  1465. };
  1466. $scope.addDevice = function (deviceID, name) {
  1467. return $http.get(urlbase + '/system/discovery')
  1468. .success(function (registry) {
  1469. $scope.discovery = [];
  1470. for (var id in registry) {
  1471. if ($scope.discovery.length === 5) {
  1472. break;
  1473. }
  1474. if (id in $scope.devices) {
  1475. continue
  1476. }
  1477. $scope.discovery.push(id);
  1478. }
  1479. })
  1480. .then(function () {
  1481. $http.get(urlbase + '/config/defaults/device').then(function (p) {
  1482. $scope.currentDevice = p.data;
  1483. $scope.currentDevice.name = name;
  1484. $scope.currentDevice.deviceID = deviceID;
  1485. $scope.editingExisting = false;
  1486. $scope.editingDefaults = false;
  1487. initShareEditing('device');
  1488. $scope.currentSharing.unrelated = $scope.folderList();
  1489. editDeviceModal();
  1490. }, $scope.emitHTTPError);
  1491. });
  1492. };
  1493. $scope.deleteDevice = function () {
  1494. $('#editDevice').modal('hide');
  1495. if (!$scope.editingExisting) {
  1496. return;
  1497. }
  1498. var id = $scope.currentDevice.deviceID
  1499. delete $scope.devices[id];
  1500. $scope.config.devices = $scope.deviceList();
  1501. for (var id in $scope.folders) {
  1502. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  1503. return n.deviceID !== $scope.currentDevice.deviceID;
  1504. });
  1505. }
  1506. $scope.saveConfig();
  1507. };
  1508. $scope.saveDevice = function () {
  1509. $('#editDevice').modal('hide');
  1510. $scope.currentDevice.addresses = $scope.currentDevice._addressesStr.split(',').map(function (x) {
  1511. return x.trim();
  1512. });
  1513. delete $scope.currentDevice._addressesStr;
  1514. if ($scope.editingDefaults) {
  1515. $scope.config.defaults.device = $scope.currentDevice;
  1516. } else {
  1517. setDeviceConfig();
  1518. }
  1519. delete $scope.currentSharing;
  1520. delete $scope.currentDevice;
  1521. $scope.saveConfig();
  1522. };
  1523. function setDeviceConfig() {
  1524. var currentID = $scope.currentDevice.deviceID;
  1525. $scope.devices[currentID] = $scope.currentDevice;
  1526. $scope.config.devices = deviceList($scope.devices);
  1527. for (var id in $scope.currentSharing.selected) {
  1528. if ($scope.currentSharing.selected[id]) {
  1529. var found = false;
  1530. for (i = 0; i < $scope.folders[id].devices.length; i++) {
  1531. if ($scope.folders[id].devices[i].deviceID === currentID) {
  1532. found = true;
  1533. // Update encryption pw
  1534. $scope.folders[id].devices[i].encryptionPassword = $scope.currentSharing.encryptionPasswords[id];
  1535. break;
  1536. }
  1537. }
  1538. if (!found) {
  1539. // Add device to folder
  1540. $scope.folders[id].devices.push({
  1541. deviceID: currentID,
  1542. encryptionPassword: $scope.currentSharing.encryptionPasswords[id],
  1543. });
  1544. }
  1545. } else {
  1546. // Remove device from folder
  1547. $scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
  1548. return n.deviceID !== currentID;
  1549. });
  1550. }
  1551. }
  1552. $scope.config.folders = folderList($scope.folders);
  1553. };
  1554. $scope.ignoreDevice = function (deviceID, pendingDevice) {
  1555. var ignoredDevice = angular.copy(pendingDevice);
  1556. ignoredDevice.deviceID = deviceID;
  1557. // Bump time
  1558. ignoredDevice.time = (new Date()).toISOString();
  1559. $scope.config.remoteIgnoredDevices.push(ignoredDevice);
  1560. $scope.saveConfig();
  1561. };
  1562. $scope.dismissPendingDevice = function (deviceID) {
  1563. $http.delete(urlbase + '/cluster/pending/devices?device=' + encodeURIComponent(deviceID));
  1564. };
  1565. $scope.unignoreDeviceFromTemporaryConfig = function (ignoredDevice) {
  1566. $scope.tmpRemoteIgnoredDevices = $scope.tmpRemoteIgnoredDevices.filter(function (existingIgnoredDevice) {
  1567. return ignoredDevice.deviceID !== existingIgnoredDevice.deviceID;
  1568. });
  1569. };
  1570. $scope.ignoredFoldersCountTmpConfig = function () {
  1571. var count = 0;
  1572. ($scope.tmpDevices || []).forEach(function (deviceCfg) {
  1573. count += deviceCfg.ignoredFolders.length;
  1574. });
  1575. return count;
  1576. };
  1577. $scope.unignoreFolderFromTemporaryConfig = function (device, ignoredFolderID) {
  1578. for (var i = 0; i < $scope.tmpDevices.length; i++) {
  1579. if ($scope.tmpDevices[i].deviceID == device) {
  1580. $scope.tmpDevices[i].ignoredFolders = $scope.tmpDevices[i].ignoredFolders.filter(function (existingIgnoredFolder) {
  1581. return existingIgnoredFolder.id !== ignoredFolderID;
  1582. });
  1583. return;
  1584. }
  1585. }
  1586. };
  1587. $scope.otherDevices = function () {
  1588. return $scope.deviceList().filter(function (n) {
  1589. return n.deviceID !== $scope.myID;
  1590. });
  1591. };
  1592. $scope.thisDevice = function () {
  1593. return $scope.devices[$scope.myID];
  1594. };
  1595. $scope.thisDeviceIn = function (l) {
  1596. for (var i = 0; i < l.length; i++) {
  1597. var n = l[i];
  1598. if (n.deviceID === $scope.myID) {
  1599. return n;
  1600. }
  1601. }
  1602. };
  1603. $scope.allDevices = function () {
  1604. var devices = $scope.otherDevices();
  1605. devices.push($scope.thisDevice());
  1606. return devices;
  1607. };
  1608. $scope.setAllDevicesPause = function (pause) {
  1609. for (var id in $scope.devices) {
  1610. $scope.devices[id].paused = pause;
  1611. };
  1612. $scope.config.devices = deviceList($scope.devices);
  1613. $scope.saveConfig();
  1614. };
  1615. $scope.isAtleastOneDevicePausedStateSetTo = function (pause) {
  1616. for (var id in $scope.devices) {
  1617. if ($scope.devices[id].paused == pause) {
  1618. return true;
  1619. }
  1620. }
  1621. return false;
  1622. };
  1623. $scope.errorList = function () {
  1624. if (!$scope.errors) {
  1625. return [];
  1626. }
  1627. return $scope.errors.filter(function (e) {
  1628. return e.when > $scope.seenError;
  1629. });
  1630. };
  1631. $scope.clearErrors = function () {
  1632. $scope.seenError = $scope.errors[$scope.errors.length - 1].when;
  1633. $http.post(urlbase + '/system/error/clear');
  1634. };
  1635. $scope.fsWatcherErrorMap = function () {
  1636. var errs = {}
  1637. $.each($scope.folders, function (id, cfg) {
  1638. if (cfg.fsWatcherEnabled && $scope.model[cfg.id] && $scope.model[id].watchError && !cfg.paused && $scope.folderStatus(cfg) !== 'stopped') {
  1639. errs[id] = $scope.model[id].watchError;
  1640. }
  1641. });
  1642. return errs;
  1643. };
  1644. $scope.friendlyDevices = function (str) {
  1645. for (var id in $scope.devices) {
  1646. str = str.replace(id, $scope.deviceName($scope.devices[id]));
  1647. }
  1648. return str;
  1649. };
  1650. $scope.folderList = function () {
  1651. return folderList($scope.folders);
  1652. };
  1653. $scope.deviceList = function () {
  1654. return deviceList($scope.devices);
  1655. };
  1656. $scope.directoryList = [];
  1657. $scope.$watch('currentFolder.path', function (newvalue) {
  1658. if (!newvalue) {
  1659. return;
  1660. }
  1661. $scope.currentFolder.path = expandTilde(newvalue);
  1662. $http.get(urlbase + '/system/browse', {
  1663. params: { current: newvalue }
  1664. }).success(function (data) {
  1665. $scope.directoryList = data;
  1666. }).error($scope.emitHTTPError);
  1667. });
  1668. $scope.$watch('currentFolder.label', function (newvalue) {
  1669. if (!newvalue || !shouldSetDefaultFolderPath()) {
  1670. return;
  1671. }
  1672. $scope.currentFolder.path = pathJoin($scope.config.defaults.folder.path, newvalue);
  1673. });
  1674. $scope.$watch('currentFolder.id', function (newvalue) {
  1675. if (!newvalue || !shouldSetDefaultFolderPath() || $scope.currentFolder.label) {
  1676. return;
  1677. }
  1678. $scope.currentFolder.path = pathJoin($scope.config.defaults.folder.path, newvalue);
  1679. });
  1680. $scope.setFSWatcherIntervalDefault = function () {
  1681. var defaultRescanIntervals = [60, 3600, 3600*24];
  1682. if (defaultRescanIntervals.indexOf($scope.currentFolder.rescanIntervalS) === -1) {
  1683. return;
  1684. }
  1685. var idx;
  1686. if ($scope.currentFolder.fsWatcherEnabled) {
  1687. idx = 1;
  1688. } else if ($scope.currentFolder.type === 'receiveencrypted') {
  1689. idx = 2;
  1690. } else {
  1691. idx = 0;
  1692. }
  1693. $scope.currentFolder.rescanIntervalS = defaultRescanIntervals[idx];
  1694. };
  1695. $scope.setDefaultsForFolderType = function () {
  1696. if ($scope.currentFolder.type === 'receiveencrypted') {
  1697. $scope.currentFolder.fsWatcherEnabled = false;
  1698. $scope.currentFolder.ignorePerms = true;
  1699. delete $scope.currentFolder.versioning;
  1700. } else {
  1701. $scope.currentFolder.fsWatcherEnabled = true;
  1702. }
  1703. $scope.setFSWatcherIntervalDefault();
  1704. };
  1705. $scope.loadFormIntoScope = function (form) {
  1706. console.log('loadFormIntoScope', form.$name);
  1707. switch (form.$name) {
  1708. case 'deviceEditor':
  1709. $scope.deviceEditor = form;
  1710. break;
  1711. case 'folderEditor':
  1712. $scope.folderEditor = form;
  1713. break;
  1714. }
  1715. };
  1716. $scope.globalChanges = function () {
  1717. $('#globalChanges').modal();
  1718. };
  1719. function editFolderModal(initialTab) {
  1720. initVersioningEditing();
  1721. $scope.currentFolder._recvEnc = $scope.currentFolder.type === 'receiveencrypted';
  1722. $scope.folderPathErrors = {};
  1723. $scope.folderEditor.$setPristine();
  1724. if (!initialTab) {
  1725. initialTab = "#folder-general";
  1726. }
  1727. $('.nav-tabs a[href="' + initialTab + '"]').tab('show');
  1728. $('#editFolder').modal().one('shown.bs.tab', function (e) {
  1729. if (e.target.attributes.href.value === "#folder-ignores") {
  1730. $('#folder-ignores textarea').focus();
  1731. }
  1732. }).one('hidden.bs.modal', function () {
  1733. window.location.hash = "";
  1734. $scope.currentFolder = {};
  1735. });
  1736. };
  1737. $scope.editFolderModalTitle = function() {
  1738. if ($scope.editingDefaults) {
  1739. return $translate.instant("Edit Folder Defaults");
  1740. }
  1741. var title = '';
  1742. if ($scope.editingExisting) {
  1743. title += $translate.instant("Edit Folder");
  1744. } else {
  1745. title += $translate.instant("Add Folder");
  1746. }
  1747. if ($scope.currentFolder.id !== '') {
  1748. title += ' (' + $scope.folderLabel($scope.currentFolder.id) + ')';
  1749. }
  1750. return title;
  1751. };
  1752. $scope.editFolderModalIcon = function() {
  1753. if ($scope.editingDefaults || $scope.editingExisting) {
  1754. return 'fas fa-pencil-alt';
  1755. }
  1756. return 'fas fa-folder';
  1757. };
  1758. function editFolder(initialTab) {
  1759. if ($scope.currentFolder.path.length > 1 && $scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
  1760. $scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
  1761. } else if (!$scope.currentFolder.path) {
  1762. // undefined path leads to invalid input field
  1763. $scope.currentFolder.path = '';
  1764. }
  1765. initShareEditing('folder');
  1766. editFolderModal(initialTab);
  1767. }
  1768. $scope.internalVersioningEnabled = function(guiVersioning) {
  1769. if (!$scope.currentFolder._guiVersioning) {
  1770. return false;
  1771. }
  1772. return ['none', 'external'].indexOf($scope.currentFolder._guiVersioning.selector) === -1;
  1773. };
  1774. function initVersioningEditing() {
  1775. $scope.currentFolder._guiVersioning = angular.copy($scope.versioningDefaults);
  1776. var currentVersioning = $scope.currentFolder.versioning;
  1777. if (!currentVersioning || !currentVersioning.type || currentVersioning.type === 'none') {
  1778. return;
  1779. }
  1780. $scope.currentFolder._guiVersioning.cleanupIntervalS = +currentVersioning.cleanupIntervalS;
  1781. $scope.currentFolder._guiVersioning.selector = currentVersioning.type;
  1782. // Apply parameters currently in use
  1783. switch (currentVersioning.type) {
  1784. case "trashcan":
  1785. $scope.currentFolder._guiVersioning.trashcanClean = +currentVersioning.params.cleanoutDays;
  1786. break;
  1787. case "simple":
  1788. $scope.currentFolder._guiVersioning.simpleKeep = +currentVersioning.params.keep;
  1789. $scope.currentFolder._guiVersioning.trashcanClean = +currentVersioning.params.cleanoutDays;
  1790. break;
  1791. case "staggered":
  1792. $scope.currentFolder._guiVersioning.staggeredMaxAge = Math.floor(+currentVersioning.params.maxAge / 86400);
  1793. $scope.currentFolder._guiVersioning.staggeredCleanInterval = +currentVersioning.params.cleanInterval;
  1794. break;
  1795. case "external":
  1796. $scope.currentFolder._guiVersioning.externalCommand = currentVersioning.params.command;
  1797. break;
  1798. }
  1799. };
  1800. $scope.editFolderExisting = function(folderCfg, initialTab) {
  1801. $scope.editingExisting = true;
  1802. $scope.editingDefaults = false;
  1803. $scope.currentFolder = angular.copy(folderCfg);
  1804. $scope.ignores.text = 'Loading...';
  1805. $scope.ignores.error = null;
  1806. $scope.ignores.disabled = true;
  1807. $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
  1808. .success(function (data) {
  1809. $scope.currentFolder.ignores = data.ignore || [];
  1810. $scope.ignores.text = $scope.currentFolder.ignores.join('\n');
  1811. $scope.ignores.error = data.error;
  1812. $scope.ignores.disabled = false;
  1813. })
  1814. .error(function (err) {
  1815. $scope.ignores.text = $translate.instant("Failed to load ignore patterns.");
  1816. $scope.emitHTTPError(err);
  1817. });
  1818. editFolder(initialTab);
  1819. };
  1820. $scope.editFolderDefaults = function() {
  1821. $http.get(urlbase + '/config/defaults/folder')
  1822. .success(function (data) {
  1823. $scope.currentFolder = data;
  1824. $scope.editingExisting = false;
  1825. $scope.editingDefaults = true;
  1826. editFolder();
  1827. })
  1828. .error($scope.emitHTTPError);
  1829. };
  1830. $scope.selectAllSharedDevices = function (state) {
  1831. var devices = $scope.currentSharing.shared;
  1832. for (var i = 0; i < devices.length; i++) {
  1833. $scope.currentSharing.selected[devices[i].deviceID] = !!state;
  1834. }
  1835. };
  1836. $scope.selectAllUnrelatedDevices = function (state) {
  1837. var devices = $scope.currentSharing.unrelated;
  1838. for (var i = 0; i < devices.length; i++) {
  1839. $scope.currentSharing.selected[devices[i].deviceID] = !!state;
  1840. }
  1841. };
  1842. $scope.addFolder = function () {
  1843. $http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
  1844. var folderID = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase();
  1845. addFolderInit(folderID).then(function() {
  1846. // Triggers the watch that sets the path
  1847. $scope.currentFolder.label = $scope.currentFolder.label;
  1848. editFolderModal();
  1849. });
  1850. });
  1851. };
  1852. $scope.addFolderAndShare = function (folderID, pendingFolder, device) {
  1853. addFolderInit(folderID).then(function() {
  1854. $scope.currentFolder.viewFlags = {
  1855. importFromOtherDevice: true
  1856. };
  1857. $scope.currentSharing.selected[device] = true;
  1858. $scope.currentFolder.label = pendingFolder.offeredBy[device].label;
  1859. for (var k in pendingFolder.offeredBy) {
  1860. if (pendingFolder.offeredBy[k].receiveEncrypted) {
  1861. $scope.currentFolder.type = "receiveencrypted";
  1862. $scope.setDefaultsForFolderType();
  1863. break;
  1864. }
  1865. }
  1866. editFolderModal();
  1867. });
  1868. };
  1869. function addFolderInit(folderID) {
  1870. $scope.editingExisting = false;
  1871. $scope.editingDefaults = false;
  1872. return $http.get(urlbase + '/config/defaults/folder').then(function(p) {
  1873. $scope.currentFolder = p.data;
  1874. $scope.currentFolder.id = folderID;
  1875. initShareEditing('folder');
  1876. $scope.currentSharing.unrelated = $scope.currentSharing.unrelated.concat($scope.currentSharing.shared);
  1877. $scope.currentSharing.shared = [];
  1878. $scope.ignores.text = '';
  1879. $scope.ignores.error = null;
  1880. $scope.ignores.disabled = false;
  1881. }, $scope.emitHTTPError);
  1882. }
  1883. $scope.shareFolderWithDevice = function (folder, device) {
  1884. var folderCfg = $scope.folders[folder];
  1885. if (folderCfg.type == "receiveencrypted" || !$scope.pendingIsRemoteEncrypted(folder, device)) {
  1886. $scope.folders[folder].devices.push({
  1887. deviceID: device
  1888. });
  1889. $scope.config.folders = folderList($scope.folders);
  1890. $scope.saveConfig();
  1891. } else {
  1892. // Open edit folder dialog to enter encryption password
  1893. $scope.editFolderExisting(folderCfg, "#folder-sharing");
  1894. $scope.currentSharing.selected[device] = true;
  1895. }
  1896. };
  1897. $scope.saveFolder = function () {
  1898. $('#editFolder').modal('hide');
  1899. var folderCfg = angular.copy($scope.currentFolder);
  1900. $scope.currentSharing.selected[$scope.myID] = true;
  1901. var newDevices = [];
  1902. folderCfg.devices.forEach(function (dev) {
  1903. if ($scope.currentSharing.selected[dev.deviceID] === true) {
  1904. dev.encryptionPassword = $scope.currentSharing.encryptionPasswords[dev.deviceID];
  1905. newDevices.push(dev);
  1906. delete $scope.currentSharing.selected[dev.deviceID];
  1907. };
  1908. });
  1909. for (var deviceID in $scope.currentSharing.selected) {
  1910. if ($scope.currentSharing.selected[deviceID] === true) {
  1911. newDevices.push({
  1912. deviceID: deviceID,
  1913. encryptionPassword: $scope.currentSharing.encryptionPasswords[deviceID],
  1914. });
  1915. }
  1916. }
  1917. folderCfg.devices = newDevices;
  1918. delete $scope.currentSharing;
  1919. if (!folderCfg.versioning) {
  1920. folderCfg.versioning = {params: {}};
  1921. }
  1922. folderCfg.versioning.type = folderCfg._guiVersioning.selector;
  1923. if ($scope.internalVersioningEnabled()) {
  1924. folderCfg.versioning.cleanupIntervalS = folderCfg._guiVersioning.cleanupIntervalS;
  1925. }
  1926. switch (folderCfg._guiVersioning.selector) {
  1927. case "trashcan":
  1928. folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean;
  1929. break;
  1930. case "simple":
  1931. folderCfg.versioning.params.keep = '' + folderCfg._guiVersioning.simpleKeep,
  1932. folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean;
  1933. break;
  1934. case "staggered":
  1935. folderCfg.versioning.params.maxAge = '' + (folderCfg._guiVersioning.staggeredMaxAge * 86400);
  1936. folderCfg.versioning.params.cleanInterval = '' + folderCfg._guiVersioning.staggeredCleanInterval;
  1937. break;
  1938. case "external":
  1939. folderCfg.versioning.params.command = '' + folderCfg._guiVersioning.externalCommand;
  1940. break;
  1941. default:
  1942. folderCfg.versioning = {type: ''};
  1943. }
  1944. delete folderCfg._guiVersioning;
  1945. if ($scope.editingDefaults) {
  1946. $scope.config.defaults.folder = folderCfg;
  1947. $scope.saveConfig();
  1948. } else {
  1949. saveFolderExisting(folderCfg);
  1950. }
  1951. };
  1952. function saveFolderExisting(folderCfg) {
  1953. var ignoresLoaded = !$scope.ignores.disabled;
  1954. var ignores = $scope.ignores.text.split('\n');
  1955. // Split always returns a minimum 1-length array even for no patterns
  1956. if (ignores.length === 1 && ignores[0] === "") {
  1957. ignores = [];
  1958. }
  1959. if (!$scope.editingExisting && ignores.length) {
  1960. folderCfg.paused = true;
  1961. };
  1962. $scope.folders[folderCfg.id] = folderCfg;
  1963. $scope.config.folders = folderList($scope.folders);
  1964. function arrayEquals(a, b) {
  1965. return a.length === b.length && a.every(function(v, i) { return v === b[i] });
  1966. }
  1967. if (ignoresLoaded && $scope.editingExisting && !arrayEquals(ignores, folderCfg.ignores)) {
  1968. saveIgnores(ignores);
  1969. };
  1970. $scope.saveConfig(function () {
  1971. if (!$scope.editingExisting && ignores.length) {
  1972. saveIgnores(ignores, function () {
  1973. $scope.setFolderPause(folderCfg.id, false);
  1974. });
  1975. }
  1976. });
  1977. };
  1978. $scope.ignoreFolder = function (device, folderID, offeringDevice) {
  1979. var ignoredFolder = {
  1980. id: folderID,
  1981. label: offeringDevice.label,
  1982. // Bump time
  1983. time: (new Date()).toISOString()
  1984. }
  1985. if (device in $scope.devices) {
  1986. $scope.devices[device].ignoredFolders.push(ignoredFolder);
  1987. $scope.saveConfig();
  1988. }
  1989. };
  1990. $scope.dismissPendingFolder = function (folderID, deviceID) {
  1991. $http.delete(urlbase + '/cluster/pending/folders?folder=' + encodeURIComponent(folderID)
  1992. + '&device=' + encodeURIComponent(deviceID));
  1993. };
  1994. $scope.sharesFolder = function (folderCfg) {
  1995. var names = [];
  1996. folderCfg.devices.forEach(function (device) {
  1997. if (device.deviceID !== $scope.myID) {
  1998. names.push($scope.deviceName($scope.devices[device.deviceID]));
  1999. }
  2000. });
  2001. names.sort();
  2002. return names.join(", ");
  2003. };
  2004. $scope.deviceFolders = function (deviceCfg) {
  2005. var folders = [];
  2006. $scope.folderList().forEach(function (folder) {
  2007. for (var i = 0; i < folder.devices.length; i++) {
  2008. if (folder.devices[i].deviceID === deviceCfg.deviceID) {
  2009. folders.push(folder.id);
  2010. break;
  2011. }
  2012. }
  2013. });
  2014. return folders;
  2015. };
  2016. $scope.folderLabel = function (folderID) {
  2017. if (!$scope.folders[folderID]) {
  2018. return folderID;
  2019. }
  2020. var label = $scope.folders[folderID].label;
  2021. return label && label.length > 0 ? label : folderID;
  2022. };
  2023. $scope.deleteFolder = function (id) {
  2024. $('#editFolder').modal('hide');
  2025. if (!$scope.editingExisting) {
  2026. return;
  2027. }
  2028. delete $scope.folders[id];
  2029. delete $scope.model[id];
  2030. $scope.config.folders = folderList($scope.folders);
  2031. recalcLocalStateTotal();
  2032. $scope.saveConfig();
  2033. };
  2034. function resetRestoreVersions() {
  2035. $scope.restoreVersions = {
  2036. folder: null,
  2037. selections: {},
  2038. versions: null,
  2039. tree: null,
  2040. errors: null,
  2041. filters: {},
  2042. massAction: function (name, action) {
  2043. $.each($scope.restoreVersions.versions, function (key) {
  2044. if (key.indexOf(name + '/') == 0 && (!$scope.restoreVersions.filters.text || key.indexOf($scope.restoreVersions.filters.text) > -1)) {
  2045. if (action == 'unset') {
  2046. delete $scope.restoreVersions.selections[key];
  2047. return;
  2048. }
  2049. var availableVersions = [];
  2050. $.each($scope.restoreVersions.filterVersions($scope.restoreVersions.versions[key]), function (idx, version) {
  2051. availableVersions.push(version.versionTime);
  2052. })
  2053. if (availableVersions.length) {
  2054. availableVersions.sort(function (a, b) { return a - b; });
  2055. if (action == 'latest') {
  2056. $scope.restoreVersions.selections[key] = availableVersions.pop();
  2057. } else if (action == 'oldest') {
  2058. $scope.restoreVersions.selections[key] = availableVersions.shift();
  2059. }
  2060. }
  2061. }
  2062. });
  2063. },
  2064. filterVersions: function (versions) {
  2065. var filteredVersions = [];
  2066. $.each(versions, function (idx, version) {
  2067. if (moment(version.versionTime).isBetween($scope.restoreVersions.filters['start'], $scope.restoreVersions.filters['end'], null, '[]')) {
  2068. filteredVersions.push(version);
  2069. }
  2070. });
  2071. return filteredVersions;
  2072. },
  2073. selectionCount: function () {
  2074. var count = 0;
  2075. $.each($scope.restoreVersions.selections, function (key, value) {
  2076. if (value) {
  2077. count++;
  2078. }
  2079. });
  2080. return count;
  2081. },
  2082. restore: function () {
  2083. $scope.restoreVersions.tree.clear();
  2084. $scope.restoreVersions.tree = null;
  2085. $scope.restoreVersions.versions = null;
  2086. var selections = {};
  2087. $.each($scope.restoreVersions.selections, function (key, value) {
  2088. if (value) {
  2089. selections[key] = value;
  2090. }
  2091. });
  2092. $scope.restoreVersions.selections = {};
  2093. $http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) {
  2094. if (Object.keys(data).length == 0) {
  2095. $('#restoreVersions').modal('hide');
  2096. } else {
  2097. $scope.restoreVersions.errors = data;
  2098. }
  2099. });
  2100. },
  2101. show: function (folder) {
  2102. $scope.restoreVersions.folder = folder;
  2103. var closed = false;
  2104. var modalShown = $q.defer();
  2105. $('#restoreVersions').modal().one('hidden.bs.modal', function () {
  2106. closed = true;
  2107. resetRestoreVersions();
  2108. }).one('shown.bs.modal', function () {
  2109. modalShown.resolve();
  2110. });
  2111. var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder))
  2112. .success(function (data) {
  2113. $.each(data, function (key, values) {
  2114. $.each(values, function (idx, value) {
  2115. value.modTime = new Date(value.modTime);
  2116. value.versionTime = new Date(value.versionTime);
  2117. });
  2118. values.sort(function (a, b) {
  2119. return b.versionTime - a.versionTime;
  2120. });
  2121. });
  2122. if (closed) return;
  2123. $scope.restoreVersions.versions = data;
  2124. });
  2125. $q.all([dataReceived, modalShown.promise]).then(function () {
  2126. $timeout(function () {
  2127. if (closed) {
  2128. resetRestoreVersions();
  2129. return;
  2130. }
  2131. $scope.restoreVersions.tree = $("#restoreTree").fancytree({
  2132. extensions: ["table", "filter"],
  2133. quicksearch: true,
  2134. filter: {
  2135. autoApply: true,
  2136. counter: true,
  2137. hideExpandedCounter: true,
  2138. hideExpanders: true,
  2139. highlight: true,
  2140. leavesOnly: false,
  2141. nodata: true,
  2142. mode: "hide"
  2143. },
  2144. table: {
  2145. indentation: 20,
  2146. nodeColumnIdx: 0,
  2147. },
  2148. debugLevel: 2,
  2149. source: buildTree($scope.restoreVersions.versions),
  2150. renderColumns: function (event, data) {
  2151. var node = data.node,
  2152. $tdList = $(node.tr).find(">td"),
  2153. template;
  2154. if (node.folder) {
  2155. template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
  2156. } else {
  2157. template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
  2158. }
  2159. var scope = $rootScope.$new(true);
  2160. scope.key = node.key;
  2161. scope.restoreVersions = $scope.restoreVersions;
  2162. $tdList.eq(1).html(
  2163. $compile(template)(scope)
  2164. );
  2165. // Force angular to redraw.
  2166. $timeout(function () {
  2167. $scope.$apply();
  2168. });
  2169. }
  2170. }).fancytree("getTree");
  2171. var minDate = moment(),
  2172. maxDate = moment(0, 'X'),
  2173. date;
  2174. // Find version window.
  2175. $.each($scope.restoreVersions.versions, function (key) {
  2176. $.each($scope.restoreVersions.versions[key], function (idx, version) {
  2177. date = moment(version.versionTime);
  2178. if (date.isBefore(minDate)) {
  2179. minDate = date;
  2180. }
  2181. if (date.isAfter(maxDate)) {
  2182. maxDate = date;
  2183. }
  2184. });
  2185. });
  2186. $scope.restoreVersions.filters['start'] = minDate;
  2187. $scope.restoreVersions.filters['end'] = maxDate;
  2188. var ranges = {
  2189. 'All time': [minDate, maxDate],
  2190. 'Today': [moment(), moment()],
  2191. 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
  2192. 'Last 7 Days': [moment().subtract(6, 'days'), moment()],
  2193. 'Last 30 Days': [moment().subtract(29, 'days'), moment()],
  2194. 'This Month': [moment().startOf('month'), moment().endOf('month')],
  2195. 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
  2196. };
  2197. // Filter out invalid ranges.
  2198. $.each(ranges, function (key, range) {
  2199. if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
  2200. delete ranges[key];
  2201. }
  2202. });
  2203. $("#restoreVersionDateRange").daterangepicker({
  2204. timePicker: true,
  2205. timePicker24Hour: true,
  2206. timePickerSeconds: true,
  2207. autoUpdateInput: true,
  2208. opens: "left",
  2209. drops: "up",
  2210. startDate: minDate,
  2211. endDate: maxDate,
  2212. minDate: minDate,
  2213. maxDate: maxDate,
  2214. ranges: ranges,
  2215. locale: {
  2216. format: 'YYYY/MM/DD HH:mm:ss',
  2217. }
  2218. }).on('apply.daterangepicker', function (ev, picker) {
  2219. $scope.restoreVersions.filters['start'] = picker.startDate;
  2220. $scope.restoreVersions.filters['end'] = picker.endDate;
  2221. // Events for this UI element are not managed by angular.
  2222. // Force angular to wake up.
  2223. $timeout(function () {
  2224. $scope.$apply();
  2225. });
  2226. });
  2227. });
  2228. });
  2229. }
  2230. };
  2231. }
  2232. resetRestoreVersions();
  2233. $scope.$watchCollection('restoreVersions.filters', function () {
  2234. if (!$scope.restoreVersions.tree) return;
  2235. $scope.restoreVersions.tree.filterNodes(function (node) {
  2236. if (node.folder) return false;
  2237. if ($scope.restoreVersions.filters.text && node.key.indexOf($scope.restoreVersions.filters.text) < 0) {
  2238. return false;
  2239. }
  2240. if ($scope.restoreVersions.filterVersions(node.data.versions).length == 0) {
  2241. return false;
  2242. }
  2243. return true;
  2244. });
  2245. });
  2246. $scope.setAPIKey = function (cfg) {
  2247. $http.get(urlbase + '/svc/random/string?length=32').success(function (data) {
  2248. cfg.apiKey = data.random;
  2249. });
  2250. };
  2251. $scope.acceptUR = function () {
  2252. $scope.config.options.urAccepted = $scope.system.urVersionMax;
  2253. $scope.config.options.urSeen = $scope.system.urVersionMax;
  2254. $scope.saveConfig();
  2255. $('#ur').modal('hide');
  2256. };
  2257. $scope.declineUR = function () {
  2258. if ($scope.config.options.urAccepted === 0) {
  2259. $scope.config.options.urAccepted = -1;
  2260. }
  2261. $scope.config.options.urSeen = $scope.system.urVersionMax;
  2262. $scope.saveConfig();
  2263. $('#ur').modal('hide');
  2264. };
  2265. $scope.showNeed = function (folder) {
  2266. $scope.neededFolder = folder;
  2267. $scope.refreshNeed(1, 10);
  2268. $('#needed').modal().one('hidden.bs.modal', function () {
  2269. $scope.needed = undefined;
  2270. $scope.neededFolder = '';
  2271. });
  2272. };
  2273. $scope.showRemoteNeed = function (device) {
  2274. resetRemoteNeed();
  2275. $scope.remoteNeedDevice = device;
  2276. $scope.deviceFolders(device).forEach(function (folder) {
  2277. var comp = $scope.completion[device.deviceID][folder];
  2278. if (comp !== undefined && comp.needItems + comp.needDeletes === 0) {
  2279. return;
  2280. }
  2281. $scope.remoteNeedFolders.push(folder);
  2282. $scope.refreshRemoteNeed(folder, 1, 10);
  2283. });
  2284. $('#remoteNeed').modal().one('hidden.bs.modal', function () {
  2285. resetRemoteNeed();
  2286. });
  2287. };
  2288. $scope.showFailed = function (folder) {
  2289. $scope.failed.folder = folder;
  2290. $scope.failed = $scope.refreshFailed(1, 10);
  2291. $('#failed').modal().one('hidden.bs.modal', function () {
  2292. $scope.failed = {};
  2293. });
  2294. };
  2295. $scope.hasFailedFiles = function (folder) {
  2296. if (!$scope.model[folder]) {
  2297. return false;
  2298. }
  2299. return $scope.model[folder].errors !== 0;
  2300. };
  2301. $scope.showLocalChanged = function (folder, folderType) {
  2302. $scope.localChangedFolder = folder;
  2303. $scope.localChangedType = folderType;
  2304. $scope.localChanged = $scope.refreshLocalChanged(1, 10);
  2305. $('#localChanged').modal().one('hidden.bs.modal', function () {
  2306. $scope.localChanged = {};
  2307. $scope.localChangedFolder = undefined;
  2308. $scope.localChangedType = undefined;
  2309. });
  2310. };
  2311. $scope.hasReceiveOnlyChanged = function (folderCfg) {
  2312. if (!folderCfg || folderCfg.type !== "receiveonly") {
  2313. return false;
  2314. }
  2315. var counts = $scope.model[folderCfg.id];
  2316. return counts && counts.receiveOnlyTotalItems > 0;
  2317. };
  2318. $scope.hasReceiveEncryptedItems = function (folderCfg) {
  2319. if (!folderCfg || folderCfg.type !== "receiveencrypted") {
  2320. return false;
  2321. }
  2322. return $scope.receiveEncryptedItemsCount(folderCfg) > 0;
  2323. };
  2324. $scope.receiveEncryptedItemsCount = function (folderCfg) {
  2325. var counts = $scope.model[folderCfg.id];
  2326. if (!counts) {
  2327. return 0;
  2328. }
  2329. return counts.receiveOnlyTotalItems - counts.receiveOnlyChangedDeletes;
  2330. }
  2331. $scope.revertOverride = function () {
  2332. $http.post(
  2333. urlbase + "/db/" + $scope.revertOverrideParams.operation +"?folder="
  2334. +encodeURIComponent($scope.revertOverrideParams.folderID));
  2335. };
  2336. $scope.revertOverrideConfirmationModal = function (type, folderID) {
  2337. var params = {
  2338. type: type,
  2339. folderID: folderID,
  2340. };
  2341. switch (type) {
  2342. case "override":
  2343. params.heading = $translate.instant("Override");
  2344. params.operation = "override";
  2345. break;
  2346. case "revert":
  2347. params.heading = $translate.instant("Revert");
  2348. params.operation = "revert";
  2349. break;
  2350. case "deleteEnc":
  2351. params.heading = $translate.instant("Delete Unexpected Items");
  2352. params.operation = "revert";
  2353. break;
  2354. }
  2355. $scope.revertOverrideParams = params;
  2356. $('#revert-override-confirmation').modal('show');
  2357. };
  2358. $scope.advanced = function () {
  2359. $scope.advancedConfig = angular.copy($scope.config);
  2360. $scope.advancedConfig.devices.sort(deviceCompare);
  2361. $scope.advancedConfig.folders.sort(folderCompare);
  2362. $('#advanced').modal('show');
  2363. };
  2364. $scope.showReportPreview = function () {
  2365. $scope.reportPreview = true;
  2366. };
  2367. $scope.refreshReportDataPreview = function (ver, diff) {
  2368. $scope.reportDataPreview = '';
  2369. if (!ver) {
  2370. return;
  2371. }
  2372. var version = parseInt(ver);
  2373. if (diff && version > 2) {
  2374. $q.all([
  2375. $http.get(urlbase + '/svc/report?version=' + version),
  2376. $http.get(urlbase + '/svc/report?version=' + (version - 1)),
  2377. ]).then(function (responses) {
  2378. var newReport = responses[0].data;
  2379. var oldReport = responses[1].data;
  2380. angular.forEach(oldReport, function (_, key) {
  2381. delete newReport[key];
  2382. });
  2383. $scope.reportDataPreview = newReport;
  2384. });
  2385. } else {
  2386. $http.get(urlbase + '/svc/report?version=' + version).success(function (data) {
  2387. $scope.reportDataPreview = data;
  2388. }).error($scope.emitHTTPError);
  2389. }
  2390. };
  2391. $scope.rescanAllFolders = function () {
  2392. $http.post(urlbase + "/db/scan");
  2393. };
  2394. $scope.rescanFolder = function (folder) {
  2395. $http.post(urlbase + "/db/scan?folder=" + encodeURIComponent(folder));
  2396. };
  2397. $scope.setAllFoldersPause = function (pause) {
  2398. var folderListCache = $scope.folderList();
  2399. for (var i = 0; i < folderListCache.length; i++) {
  2400. folderListCache[i].paused = pause;
  2401. }
  2402. $scope.config.folders = folderList(folderListCache);
  2403. $scope.saveConfig();
  2404. };
  2405. $scope.isAtleastOneFolderPausedStateSetTo = function (pause) {
  2406. var folderListCache = $scope.folderList();
  2407. for (var i = 0; i < folderListCache.length; i++) {
  2408. if (folderListCache[i].paused == pause) {
  2409. return true;
  2410. }
  2411. }
  2412. return false;
  2413. };
  2414. $scope.activateAllFsWatchers = function () {
  2415. var folders = $scope.folderList();
  2416. $.each(folders, function (i) {
  2417. if (folders[i].fsWatcherEnabled) {
  2418. return;
  2419. }
  2420. folders[i].fsWatcherEnabled = true;
  2421. if (folders[i].rescanIntervalS === 0) {
  2422. return;
  2423. }
  2424. // Delay full scans, but scan at least once per day
  2425. folders[i].rescanIntervalS *= 60;
  2426. if (folders[i].rescanIntervalS > 86400) {
  2427. folders[i].rescanIntervalS = 86400;
  2428. }
  2429. });
  2430. $scope.config.folders = folders;
  2431. $scope.saveConfig();
  2432. };
  2433. $scope.bumpFile = function (folder, file) {
  2434. var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
  2435. // In order to get the right view of data in the response.
  2436. url += "&page=" + $scope.needed.page;
  2437. url += "&perpage=" + $scope.needed.perpage;
  2438. $http.post(url).success(function (data) {
  2439. if ($scope.neededFolder === folder) {
  2440. console.log("bumpFile", folder, data);
  2441. parseNeeded(data);
  2442. }
  2443. }).error($scope.emitHTTPError);
  2444. };
  2445. $scope.versionString = function () {
  2446. if (!$scope.version.version) {
  2447. return '';
  2448. }
  2449. var os = {
  2450. 'darwin': 'macOS',
  2451. 'dragonfly': 'DragonFly BSD',
  2452. 'freebsd': 'FreeBSD',
  2453. 'openbsd': 'OpenBSD',
  2454. 'netbsd': 'NetBSD',
  2455. 'linux': 'Linux',
  2456. 'windows': 'Windows',
  2457. 'solaris': 'Solaris'
  2458. }[$scope.version.os] || $scope.version.os;
  2459. var arch = {
  2460. '386': '32-bit Intel/AMD',
  2461. 'amd64': '64-bit Intel/AMD',
  2462. 'arm': '32-bit ARM',
  2463. 'arm64': '64-bit ARM',
  2464. 'ppc64': '64-bit PowerPC',
  2465. 'ppc64le': '64-bit PowerPC (LE)',
  2466. 'mips': '32-bit MIPS',
  2467. 'mipsle': '32-bit MIPS (LE)',
  2468. 'mips64': '64-bit MIPS',
  2469. 'mips64le': '64-bit MIPS (LE)',
  2470. 'riscv64': '64-bit RISC-V',
  2471. 's390x': '64-bit z/Architecture',
  2472. }[$scope.version.arch] || $scope.version.arch;
  2473. return $scope.version.version + ', ' + os + ' (' + arch + ')';
  2474. };
  2475. $scope.inputTypeFor = function (key, value) {
  2476. if (key.substr(0, 1) === '_') {
  2477. return 'skip';
  2478. }
  2479. if (value === null) {
  2480. return 'null';
  2481. }
  2482. if (typeof value === 'number') {
  2483. return 'number';
  2484. }
  2485. if (typeof value === 'boolean') {
  2486. return 'checkbox';
  2487. }
  2488. if (value instanceof Array) {
  2489. return 'list';
  2490. }
  2491. if (typeof value === 'object') {
  2492. return 'skip';
  2493. }
  2494. return 'text';
  2495. };
  2496. $scope.themeName = function (theme) {
  2497. return theme.replace('-', ' ').replace(/(?:^|\s)\S/g, function (a) {
  2498. return a.toUpperCase();
  2499. });
  2500. };
  2501. $scope.modalLoaded = function () {
  2502. // once all modal elements have been processed
  2503. if ($('modal').length === 0) {
  2504. // pseudo main. called on all definitions assigned
  2505. initController();
  2506. }
  2507. };
  2508. $scope.toggleUnits = function () {
  2509. $scope.metricRates = !$scope.metricRates;
  2510. try {
  2511. window.localStorage["metricRates"] = $scope.metricRates;
  2512. } catch (exception) { }
  2513. };
  2514. $scope.sizeOf = function (dict) {
  2515. if (dict === undefined) {
  2516. return 0;
  2517. }
  2518. return Object.keys(dict).length;
  2519. };
  2520. $scope.dismissNotification = function (id) {
  2521. var idx = $scope.config.options.unackedNotificationIDs.indexOf(id);
  2522. if (idx > -1) {
  2523. $scope.config.options.unackedNotificationIDs.splice(idx, 1);
  2524. $scope.saveConfig();
  2525. }
  2526. };
  2527. $scope.abbreviatedError = function (addr) {
  2528. var status = $scope.system.lastDialStatus[addr];
  2529. if (!status || !status.error) {
  2530. return null;
  2531. }
  2532. var time = $filter('date')(status.when, "HH:mm:ss")
  2533. var err = status.error.replace(/.+: /, '');
  2534. return err + " (" + time + ")";
  2535. };
  2536. $scope.setCrashReportingEnabled = function (enabled) {
  2537. $scope.config.options.crashReportingEnabled = enabled;
  2538. $scope.saveConfig();
  2539. };
  2540. $scope.isUnixAddress = function (address) {
  2541. return address != null &&
  2542. (address.indexOf('/') == 0 ||
  2543. address.indexOf('unix://') == 0 ||
  2544. address.indexOf('unixs://') == 0);
  2545. };
  2546. })
  2547. .directive('shareTemplate', function () {
  2548. return {
  2549. templateUrl: 'syncthing/core/editShareTemplate.html',
  2550. scope: {
  2551. selected: '=',
  2552. encryptionPasswords: '=',
  2553. id: '@',
  2554. label: '@',
  2555. folderType: '@',
  2556. untrusted: '=',
  2557. },
  2558. link: function(scope, elem, attrs) {
  2559. var plain = false;
  2560. scope.togglePasswordVisibility = function() {
  2561. scope.plain = !scope.plain;
  2562. };
  2563. },
  2564. }
  2565. });