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