Add folder synchronization status.
[src/xds/xds-server.git] / webapp / src / app / services / config.service.ts
1 import { Injectable, OnInit } from '@angular/core';
2 import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
3 import { Location } from '@angular/common';
4 import { CookieService } from 'ngx-cookie';
5 import { Observable } from 'rxjs/Observable';
6 import { Subscriber } from 'rxjs/Subscriber';
7 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
8
9 // Import RxJs required methods
10 import 'rxjs/add/operator/map';
11 import 'rxjs/add/operator/catch';
12 import 'rxjs/add/observable/throw';
13 import 'rxjs/add/operator/mergeMap';
14
15
16 import { XDSServerService, IXDSFolderConfig } from "../services/xdsserver.service";
17 import { XDSAgentService } from "../services/xdsagent.service";
18 import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../services/syncthing.service";
19 import { AlertService, IAlert } from "../services/alert.service";
20 import { UtilsService } from "../services/utils.service";
21
22 export enum ProjectType {
23     NATIVE_PATHMAP = 1,
24     SYNCTHING = 2
25 }
26
27 export var ProjectTypes = [
28     { value: ProjectType.NATIVE_PATHMAP, display: "Path mapping" },
29     { value: ProjectType.SYNCTHING, display: "Cloud Sync" }
30 ];
31
32 export interface IProject {
33     id?: string;
34     label: string;
35     pathClient: string;
36     pathServer?: string;
37     type: ProjectType;
38     status?: string;
39     isInSync?: boolean;
40     serverPrjDef?: IXDSFolderConfig;
41     isExpanded?: boolean;
42     visible?: boolean;
43     defaultSdkID?: string;
44 }
45
46 export interface IXDSAgentConfig {
47     URL: string;
48     retry: number;
49 }
50
51 export interface ILocalSTConfig {
52     ID: string;
53     URL: string;
54     retry: number;
55     tilde: string;
56 }
57
58 export interface IxdsAgentPackage {
59     os: string;
60     arch: string;
61     version: string;
62     url: string;
63 }
64
65 export interface IConfig {
66     xdsServerURL: string;
67     xdsAgent: IXDSAgentConfig;
68     xdsAgentPackages: IxdsAgentPackage[];
69     projectsRootDir: string;
70     projects: IProject[];
71     localSThg: ILocalSTConfig;
72 }
73
74 @Injectable()
75 export class ConfigService {
76
77     public conf: Observable<IConfig>;
78
79     private confSubject: BehaviorSubject<IConfig>;
80     private confStore: IConfig;
81     private AgentConnectObs = null;
82     private stConnectObs = null;
83
84     constructor(private _window: Window,
85         private cookie: CookieService,
86         private xdsServerSvr: XDSServerService,
87         private xdsAgentSvr: XDSAgentService,
88         private stSvr: SyncthingService,
89         private alert: AlertService,
90         private utils: UtilsService,
91     ) {
92         this.load();
93         this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
94         this.conf = this.confSubject.asObservable();
95
96         // force to load projects
97         this.loadProjects();
98     }
99
100     // Load config
101     load() {
102         // Try to retrieve previous config from cookie
103         let cookConf = this.cookie.getObject("xds-config");
104         if (cookConf != null) {
105             this.confStore = <IConfig>cookConf;
106         } else {
107             // Set default config
108             this.confStore = {
109                 xdsServerURL: this._window.location.origin + '/api/v1',
110                 xdsAgent: {
111                     URL: 'http://localhost:8000',
112                     retry: 10,
113                 },
114                 xdsAgentPackages: [],
115                 projectsRootDir: "",
116                 projects: [],
117                 localSThg: {
118                     ID: null,
119                     URL: "http://localhost:8384",
120                     retry: 10,    // 10 seconds
121                     tilde: "",
122                 }
123             };
124         }
125
126         // Update XDS Agent tarball url
127         this.xdsServerSvr.getXdsAgentInfo().subscribe(nfo => {
128             this.confStore.xdsAgentPackages = [];
129             nfo.tarballs && nfo.tarballs.forEach(el =>
130                 this.confStore.xdsAgentPackages.push({
131                     os: el.os,
132                     arch: el.arch,
133                     version: el.version,
134                     url: el.fileUrl
135                 })
136             );
137             this.confSubject.next(Object.assign({}, this.confStore));
138         });
139
140         // Update Project data
141         this.xdsServerSvr.FolderStateChange$.subscribe(prj => {
142             let i = this._getProjectIdx(prj.id);
143             if (i >= 0) {
144                 // XXX for now, only isInSync and status may change
145                 this.confStore.projects[i].isInSync = prj.isInSync;
146                 this.confStore.projects[i].status = prj.status;
147                 this.confSubject.next(Object.assign({}, this.confStore));
148             }
149         });
150     }
151
152     // Save config into cookie
153     save() {
154         // Notify subscribers
155         this.confSubject.next(Object.assign({}, this.confStore));
156
157         // Don't save projects in cookies (too big!)
158         let cfg = Object.assign({}, this.confStore);
159         delete (cfg.projects);
160         this.cookie.putObject("xds-config", cfg);
161     }
162
163     loadProjects() {
164         // Setup connection with local XDS agent
165         if (this.AgentConnectObs) {
166             try {
167                 this.AgentConnectObs.unsubscribe();
168             } catch (err) { }
169             this.AgentConnectObs = null;
170         }
171
172         let cfg = this.confStore.xdsAgent;
173         this.AgentConnectObs = this.xdsAgentSvr.connect(cfg.retry, cfg.URL)
174             .subscribe((sts) => {
175                 //console.log("Agent sts", sts);
176                 // FIXME: load projects from local XDS Agent and
177                 //  not directly from local syncthing
178                 this._loadProjectFromLocalST();
179
180             }, error => {
181                 if (error.indexOf("XDS local Agent not responding") !== -1) {
182                     let msg = "<span><strong>" + error + "<br></strong>";
183                     msg += "You may need to download and execute XDS-Agent.<br>";
184
185                     let os = this.utils.getOSName(true);
186                     let zurl = this.confStore.xdsAgentPackages && this.confStore.xdsAgentPackages.filter(elem => elem.os === os);
187                     if (zurl && zurl.length) {
188                         msg += " Download XDS-Agent tarball for " + zurl[0].os + " host OS ";
189                         msg += "<a class=\"fa fa-download\" href=\"" + zurl[0].url + "\" target=\"_blank\"></a>";
190                     }
191                     msg += "</span>";
192                     this.alert.error(msg);
193                 } else {
194                     this.alert.error(error);
195                 }
196             });
197     }
198
199     private _loadProjectFromLocalST() {
200         // Remove previous subscriber if existing
201         if (this.stConnectObs) {
202             try {
203                 this.stConnectObs.unsubscribe();
204             } catch (err) { }
205             this.stConnectObs = null;
206         }
207
208         // FIXME: move this code and all logic about syncthing inside XDS Agent
209         // Setup connection with local SyncThing
210         let retry = this.confStore.localSThg.retry;
211         let url = this.confStore.localSThg.URL;
212         this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => {
213             this.confStore.localSThg.ID = sts.ID;
214             this.confStore.localSThg.tilde = sts.tilde;
215             if (this.confStore.projectsRootDir === "") {
216                 this.confStore.projectsRootDir = sts.tilde;
217             }
218
219             // Rebuild projects definition from local and remote syncthing
220             this.confStore.projects = [];
221
222             this.xdsServerSvr.getProjects().subscribe(remotePrj => {
223                 this.stSvr.getProjects().subscribe(localPrj => {
224                     remotePrj.forEach(rPrj => {
225                         let lPrj = localPrj.filter(item => item.id === rPrj.id);
226                         if (lPrj.length > 0 || rPrj.type === ProjectType.NATIVE_PATHMAP) {
227                             this._addProject(rPrj, true);
228                         }
229                     });
230                     this.confSubject.next(Object.assign({}, this.confStore));
231                 }), error => this.alert.error('Could not load initial state of local projects.');
232             }), error => this.alert.error('Could not load initial state of remote projects.');
233
234         }, error => {
235             if (error.indexOf("Syncthing local daemon not responding") !== -1) {
236                 let msg = "<span><strong>" + error + "<br></strong>";
237                 msg += "Please check that local XDS-Agent is running.<br>";
238                 msg += "</span>";
239                 this.alert.error(msg);
240             } else {
241                 this.alert.error(error);
242             }
243         });
244     }
245
246     set syncToolURL(url: string) {
247         this.confStore.localSThg.URL = url;
248         this.save();
249     }
250
251     set xdsAgentRetry(r: number) {
252         this.confStore.localSThg.retry = r;
253         this.confStore.xdsAgent.retry = r;
254         this.save();
255     }
256
257     set xdsAgentUrl(url: string) {
258         this.confStore.xdsAgent.URL = url;
259         this.save();
260     }
261
262
263     set projectsRootDir(p: string) {
264         if (p.charAt(0) === '~') {
265             p = this.confStore.localSThg.tilde + p.substring(1);
266         }
267         this.confStore.projectsRootDir = p;
268         this.save();
269     }
270
271     getLabelRootName(): string {
272         let id = this.confStore.localSThg.ID;
273         if (!id || id === "") {
274             return null;
275         }
276         return id.slice(0, 15);
277     }
278
279     addProject(prj: IProject): Observable<IProject> {
280         // Substitute tilde with to user home path
281         let pathCli = prj.pathClient.trim();
282         if (pathCli.charAt(0) === '~') {
283             pathCli = this.confStore.localSThg.tilde + pathCli.substring(1);
284
285             // Must be a full path (on Linux or Windows)
286         } else if (!((pathCli.charAt(0) === '/') ||
287             (pathCli.charAt(1) === ':' && (pathCli.charAt(2) === '\\' || pathCli.charAt(2) === '/')))) {
288             pathCli = this.confStore.projectsRootDir + '/' + pathCli;
289         }
290
291         let xdsPrj: IXDSFolderConfig = {
292             id: "",
293             label: prj.label || "",
294             path: pathCli,
295             type: prj.type,
296             defaultSdkID: prj.defaultSdkID,
297             dataPathMap: {
298                 serverPath: prj.pathServer,
299             },
300             dataCloudSync: {
301                 syncThingID: this.confStore.localSThg.ID,
302             }
303         };
304         // Send config to XDS server
305         let newPrj = prj;
306         return this.xdsServerSvr.addProject(xdsPrj)
307             .flatMap(resStRemotePrj => {
308                 xdsPrj = resStRemotePrj;
309                 if (xdsPrj.type === ProjectType.SYNCTHING) {
310                     // FIXME REWORK local ST config
311                     //  move logic to server side tunneling-back by WS
312                     let stData = xdsPrj.dataCloudSync;
313
314                     // Now setup local config
315                     let stLocPrj: ISyncThingProject = {
316                         id: xdsPrj.id,
317                         label: xdsPrj.label,
318                         path: xdsPrj.path,
319                         serverSyncThingID: stData.builderSThgID
320                     };
321
322                     // Set local Syncthing config
323                     return this.stSvr.addProject(stLocPrj);
324
325                 } else {
326                     return Observable.of(null);
327                 }
328             })
329             .map(resStLocalPrj => {
330                 this._addProject(xdsPrj);
331                 return newPrj;
332             });
333     }
334
335     deleteProject(prj: IProject): Observable<IProject> {
336         let idx = this._getProjectIdx(prj.id);
337         let delPrj = prj;
338         if (idx === -1) {
339             throw new Error("Invalid project id (id=" + prj.id + ")");
340         }
341         return this.xdsServerSvr.deleteProject(prj.id)
342             .flatMap(res => {
343                 if (prj.type === ProjectType.SYNCTHING) {
344                     return this.stSvr.deleteProject(prj.id);
345                 }
346                 return Observable.of(null);
347             })
348             .map(res => {
349                 this.confStore.projects.splice(idx, 1);
350                 return delPrj;
351             });
352     }
353
354     syncProject(prj: IProject): Observable<string> {
355         let idx = this._getProjectIdx(prj.id);
356         if (idx === -1) {
357             throw new Error("Invalid project id (id=" + prj.id + ")");
358         }
359         return this.xdsServerSvr.syncProject(prj.id);
360     }
361
362     private _getProjectIdx(id: string): number {
363         return this.confStore.projects.findIndex((item) => item.id === id);
364     }
365
366     private _addProject(rPrj: IXDSFolderConfig, noNext?: boolean) {
367
368         // Convert XDSFolderConfig to IProject
369         let pp: IProject = {
370             id: rPrj.id,
371             label: rPrj.label,
372             pathClient: rPrj.path,
373             pathServer: rPrj.dataPathMap.serverPath,
374             type: rPrj.type,
375             status: rPrj.status,
376             isInSync: rPrj.isInSync,
377             defaultSdkID: rPrj.defaultSdkID,
378             serverPrjDef: Object.assign({}, rPrj),  // do a copy
379         };
380
381         // add new project
382         this.confStore.projects.push(pp);
383
384         // sort project array
385         this.confStore.projects.sort((a, b) => {
386             if (a.label < b.label) {
387                 return -1;
388             }
389             if (a.label > b.label) {
390                 return 1;
391             }
392             return 0;
393         });
394
395         // FIXME: maybe reduce subject to only .project
396         //this.confSubject.next(Object.assign({}, this.confStore).project);
397         if (!noNext) {
398             this.confSubject.next(Object.assign({}, this.confStore));
399         }
400     }
401 }