syncthingController.js 120 KB

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