syncthingController.js 136 KB

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