Auto detect XDS-Agent tarballs and fix URL.
[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, IXDSConfigProject } 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 = 1,
24     SYNCTHING = 2
25 }
26
27 export interface INativeProject {
28     // TODO
29 }
30
31 export interface IProject {
32     id?: string;
33     label: string;
34     path: string;
35     type: ProjectType;
36     remotePrjDef?: INativeProject | ISyncThingProject;
37     localPrjDef?: any;
38     isExpanded?: boolean;
39     visible?: boolean;
40     defaultSdkID?: string;
41 }
42
43 export interface IXDSAgentConfig {
44     URL: string;
45     retry: number;
46 }
47
48 export interface ILocalSTConfig {
49     ID: string;
50     URL: string;
51     retry: number;
52     tilde: string;
53 }
54
55 export interface IConfig {
56     xdsServerURL: string;
57     xdsAgent: IXDSAgentConfig;
58     xdsAgentZipUrl: string;
59     projectsRootDir: string;
60     projects: IProject[];
61     localSThg: ILocalSTConfig;
62 }
63
64 @Injectable()
65 export class ConfigService {
66
67     public conf: Observable<IConfig>;
68
69     private confSubject: BehaviorSubject<IConfig>;
70     private confStore: IConfig;
71     private AgentConnectObs = null;
72     private stConnectObs = null;
73     private xdsAgentZipUrl = "";
74
75     constructor(private _window: Window,
76         private cookie: CookieService,
77         private xdsServerSvr: XDSServerService,
78         private xdsAgentSvr: XDSAgentService,
79         private stSvr: SyncthingService,
80         private alert: AlertService,
81         private utils: UtilsService,
82     ) {
83         this.load();
84         this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
85         this.conf = this.confSubject.asObservable();
86
87         // force to load projects
88         this.loadProjects();
89     }
90
91     // Load config
92     load() {
93         // Try to retrieve previous config from cookie
94         let cookConf = this.cookie.getObject("xds-config");
95         if (cookConf != null) {
96             this.confStore = <IConfig>cookConf;
97         } else {
98             // Set default config
99             this.confStore = {
100                 xdsServerURL: this._window.location.origin + '/api/v1',
101                 xdsAgent: {
102                     URL: 'http://localhost:8000',
103                     retry: 10,
104                 },
105                 xdsAgentZipUrl: "",
106                 projectsRootDir: "",
107                 projects: [],
108                 localSThg: {
109                     ID: null,
110                     URL: "http://localhost:8384",
111                     retry: 10,    // 10 seconds
112                     tilde: "",
113                 }
114             };
115         }
116
117         // Update XDS Agent tarball url
118         this.confStore.xdsAgentZipUrl = "";
119         this.xdsServerSvr.getXdsAgentInfo().subscribe(nfo => {
120             let os = this.utils.getOSName(true);
121             let zurl = nfo.tarballs.filter(elem => elem.os === os);
122             if (zurl && zurl.length) {
123                 this.confStore.xdsAgentZipUrl = zurl[0].fileUrl;
124                 this.confSubject.next(Object.assign({}, this.confStore));
125             }
126         });
127     }
128
129     // Save config into cookie
130     save() {
131         // Notify subscribers
132         this.confSubject.next(Object.assign({}, this.confStore));
133
134         // Don't save projects in cookies (too big!)
135         let cfg = Object.assign({}, this.confStore);
136         delete (cfg.projects);
137         this.cookie.putObject("xds-config", cfg);
138     }
139
140     loadProjects() {
141         // Setup connection with local XDS agent
142         if (this.AgentConnectObs) {
143             try {
144                 this.AgentConnectObs.unsubscribe();
145             } catch (err) { }
146             this.AgentConnectObs = null;
147         }
148
149         let cfg = this.confStore.xdsAgent;
150         this.AgentConnectObs = this.xdsAgentSvr.connect(cfg.retry, cfg.URL)
151             .subscribe((sts) => {
152                 //console.log("Agent sts", sts);
153                 // FIXME: load projects from local XDS Agent and
154                 //  not directly from local syncthing
155                 this._loadProjectFromLocalST();
156
157             }, error => {
158                 if (error.indexOf("XDS local Agent not responding") !== -1) {
159                     let msg = "<span><strong>" + error + "<br></strong>";
160                     msg += "You may need to download and execute XDS-Agent.<br>";
161                     if (this.confStore.xdsAgentZipUrl !== "") {
162                         msg += "<a class=\"fa fa-download\" href=\"" + this.confStore.xdsAgentZipUrl + "\" target=\"_blank\"></a>";
163                         msg += " Download XDS-Agent tarball.";
164                     }
165                     msg += "</span>";
166                     this.alert.error(msg);
167                 } else {
168                     this.alert.error(error);
169                 }
170             });
171     }
172
173     private _loadProjectFromLocalST() {
174         // Remove previous subscriber if existing
175         if (this.stConnectObs) {
176             try {
177                 this.stConnectObs.unsubscribe();
178             } catch (err) { }
179             this.stConnectObs = null;
180         }
181
182         // FIXME: move this code and all logic about syncthing inside XDS Agent
183         // Setup connection with local SyncThing
184         let retry = this.confStore.localSThg.retry;
185         let url = this.confStore.localSThg.URL;
186         this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => {
187             this.confStore.localSThg.ID = sts.ID;
188             this.confStore.localSThg.tilde = sts.tilde;
189             if (this.confStore.projectsRootDir === "") {
190                 this.confStore.projectsRootDir = sts.tilde;
191             }
192
193             // Rebuild projects definition from local and remote syncthing
194             this.confStore.projects = [];
195
196             this.xdsServerSvr.getProjects().subscribe(remotePrj => {
197                 this.stSvr.getProjects().subscribe(localPrj => {
198                     remotePrj.forEach(rPrj => {
199                         let lPrj = localPrj.filter(item => item.id === rPrj.id);
200                         if (lPrj.length > 0) {
201                             let pp: IProject = {
202                                 id: rPrj.id,
203                                 label: rPrj.label,
204                                 path: rPrj.path,
205                                 type: ProjectType.SYNCTHING,    // FIXME support other types
206                                 remotePrjDef: Object.assign({}, rPrj),
207                                 localPrjDef: Object.assign({}, lPrj[0]),
208                             };
209                             this.confStore.projects.push(pp);
210                         }
211                     });
212                     this.confSubject.next(Object.assign({}, this.confStore));
213                 }), error => this.alert.error('Could not load initial state of local projects.');
214             }), error => this.alert.error('Could not load initial state of remote projects.');
215
216         }, error => {
217             if (error.indexOf("Syncthing local daemon not responding") !== -1) {
218                 let msg = "<span><strong>" + error + "<br></strong>";
219                 msg += "Please check that local XDS-Agent is running.<br>";
220                 msg += "</span>";
221                 this.alert.error(msg);
222             } else {
223                 this.alert.error(error);
224             }
225         });
226     }
227
228     set syncToolURL(url: string) {
229         this.confStore.localSThg.URL = url;
230         this.save();
231     }
232
233     set xdsAgentRetry(r: number) {
234         this.confStore.localSThg.retry = r;
235         this.confStore.xdsAgent.retry = r;
236         this.save();
237     }
238
239     set xdsAgentUrl(url: string) {
240         this.confStore.xdsAgent.URL = url;
241         this.save();
242     }
243
244
245     set projectsRootDir(p: string) {
246         if (p.charAt(0) === '~') {
247             p = this.confStore.localSThg.tilde + p.substring(1);
248         }
249         this.confStore.projectsRootDir = p;
250         this.save();
251     }
252
253     getLabelRootName(): string {
254         let id = this.confStore.localSThg.ID;
255         if (!id || id === "") {
256             return null;
257         }
258         return id.slice(0, 15);
259     }
260
261     addProject(prj: IProject) {
262         // Substitute tilde with to user home path
263         prj.path = prj.path.trim();
264         if (prj.path.charAt(0) === '~') {
265             prj.path = this.confStore.localSThg.tilde + prj.path.substring(1);
266
267             // Must be a full path (on Linux or Windows)
268         } else if (!((prj.path.charAt(0) === '/') ||
269             (prj.path.charAt(1) === ':' && (prj.path.charAt(2) === '\\' || prj.path.charAt(2) === '/')))) {
270             prj.path = this.confStore.projectsRootDir + '/' + prj.path;
271         }
272
273         if (prj.id == null) {
274             // FIXME - must be done on server side
275             let prefix = this.getLabelRootName() || new Date().toISOString();
276             let splath = prj.path.split('/');
277             prj.id = prefix + "_" + splath[splath.length - 1];
278         }
279
280         if (this._getProjectIdx(prj.id) !== -1) {
281             this.alert.warning("Project already exist (id=" + prj.id + ")", true);
282             return;
283         }
284
285         // TODO - support others project types
286         if (prj.type !== ProjectType.SYNCTHING) {
287             this.alert.error('Project type not supported yet (type: ' + prj.type + ')');
288             return;
289         }
290
291         let sdkPrj: IXDSConfigProject = {
292             id: prj.id,
293             label: prj.label,
294             path: prj.path,
295             hostSyncThingID: this.confStore.localSThg.ID,
296             defaultSdkID: prj.defaultSdkID,
297         };
298
299         // Send config to XDS server
300         let newPrj = prj;
301         this.xdsServerSvr.addProject(sdkPrj)
302             .subscribe(resStRemotePrj => {
303                 newPrj.remotePrjDef = resStRemotePrj;
304
305                 // FIXME REWORK local ST config
306                 //  move logic to server side tunneling-back by WS
307
308                 // Now setup local config
309                 let stLocPrj: ISyncThingProject = {
310                     id: sdkPrj.id,
311                     label: sdkPrj.label,
312                     path: sdkPrj.path,
313                     remoteSyncThingID: resStRemotePrj.builderSThgID
314                 };
315
316                 // Set local Syncthing config
317                 this.stSvr.addProject(stLocPrj)
318                     .subscribe(resStLocalPrj => {
319                         newPrj.localPrjDef = resStLocalPrj;
320
321                         // FIXME: maybe reduce subject to only .project
322                         //this.confSubject.next(Object.assign({}, this.confStore).project);
323                         this.confStore.projects.push(Object.assign({}, newPrj));
324                         this.confSubject.next(Object.assign({}, this.confStore));
325                     },
326                     err => {
327                         this.alert.error("Configuration local ERROR: " + err);
328                     });
329             },
330             err => {
331                 this.alert.error("Configuration remote ERROR: " + err);
332             });
333     }
334
335     deleteProject(prj: IProject) {
336         let idx = this._getProjectIdx(prj.id);
337         if (idx === -1) {
338             throw new Error("Invalid project id (id=" + prj.id + ")");
339         }
340         this.xdsServerSvr.deleteProject(prj.id)
341             .subscribe(res => {
342                 this.stSvr.deleteProject(prj.id)
343                     .subscribe(res => {
344                         this.confStore.projects.splice(idx, 1);
345                     }, err => {
346                         this.alert.error("Delete local ERROR: " + err);
347                     });
348             }, err => {
349                 this.alert.error("Delete remote ERROR: " + err);
350             });
351     }
352
353     private _getProjectIdx(id: string): number {
354         return this.confStore.projects.findIndex((item) => item.id === id);
355     }
356
357 }