Initial main commit.
[src/xds/xds-server.git] / webapp / src / app / common / 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 "../common/xdsserver.service";
17 import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../common/syncthing.service";
18 import { AlertService, IAlert } from "../common/alert.service";
19
20 export enum ProjectType {
21     NATIVE = 1,
22     SYNCTHING = 2
23 }
24
25 export interface INativeProject {
26     // TODO
27 }
28
29 export interface IProject {
30     id?: string;
31     label: string;
32     path: string;
33     type: ProjectType;
34     remotePrjDef?: INativeProject | ISyncThingProject;
35     localPrjDef?: any;
36     isExpanded?: boolean;
37     visible?: boolean;
38 }
39
40 export interface ILocalSTConfig {
41     ID: string;
42     URL: string;
43     retry: number;
44     tilde: string;
45 }
46
47 export interface IConfig {
48     xdsServerURL: string;
49     projectsRootDir: string;
50     projects: IProject[];
51     localSThg: ILocalSTConfig;
52 }
53
54 @Injectable()
55 export class ConfigService {
56
57     public conf: Observable<IConfig>;
58
59     private confSubject: BehaviorSubject<IConfig>;
60     private confStore: IConfig;
61     private stConnectObs = null;
62
63     constructor(private _window: Window,
64         private cookie: CookieService,
65         private sdkSvr: XDSServerService,
66         private stSvr: SyncthingService,
67         private alert: AlertService,
68     ) {
69         this.load();
70         this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore);
71         this.conf = this.confSubject.asObservable();
72
73         // force to load projects
74         this.loadProjects();
75     }
76
77     // Load config
78     load() {
79         // Try to retrieve previous config from cookie
80         let cookConf = this.cookie.getObject("xds-config");
81         if (cookConf != null) {
82             this.confStore = <IConfig>cookConf;
83         } else {
84             // Set default config
85             this.confStore = {
86                 xdsServerURL: this._window.location.origin + '/api/v1',
87                 projectsRootDir: "",
88                 projects: [],
89                 localSThg: {
90                     ID: null,
91                     URL: "http://localhost:8384",
92                     retry: 10,    // 10 seconds
93                     tilde: "",
94                 }
95             };
96         }
97     }
98
99     // Save config into cookie
100     save() {
101         // Notify subscribers
102         this.confSubject.next(Object.assign({}, this.confStore));
103
104         // Don't save projects in cookies (too big!)
105         let cfg = this.confStore;
106         delete(cfg.projects);
107         this.cookie.putObject("xds-config", cfg);
108     }
109
110     loadProjects() {
111         // Remove previous subscriber if existing
112         if (this.stConnectObs) {
113             try {
114                 this.stConnectObs.unsubscribe();
115             } catch (err) { }
116             this.stConnectObs = null;
117         }
118
119         // First setup connection with local SyncThing
120         let retry = this.confStore.localSThg.retry;
121         let url = this.confStore.localSThg.URL;
122         this.stConnectObs = this.stSvr.connect(retry, url).subscribe((sts) => {
123             this.confStore.localSThg.ID = sts.ID;
124             this.confStore.localSThg.tilde = sts.tilde;
125             if (this.confStore.projectsRootDir === "") {
126                 this.confStore.projectsRootDir = sts.tilde;
127             }
128
129             // Rebuild projects definition from local and remote syncthing
130             this.confStore.projects = [];
131
132             this.sdkSvr.getProjects().subscribe(remotePrj => {
133                 this.stSvr.getProjects().subscribe(localPrj => {
134                     remotePrj.forEach(rPrj => {
135                         let lPrj = localPrj.filter(item => item.id === rPrj.id);
136                         if (lPrj.length > 0) {
137                             let pp: IProject = {
138                                 id: rPrj.id,
139                                 label: rPrj.label,
140                                 path: rPrj.path,
141                                 type: ProjectType.SYNCTHING,    // FIXME support other types
142                                 remotePrjDef: Object.assign({}, rPrj),
143                                 localPrjDef: Object.assign({}, lPrj[0]),
144                             };
145                             this.confStore.projects.push(pp);
146                         }
147                     });
148                     this.confSubject.next(Object.assign({}, this.confStore));
149                 }), error => this.alert.error('Could not load initial state of local projects.');
150             }), error => this.alert.error('Could not load initial state of remote projects.');
151
152         }, error => this.alert.error(error));
153     }
154
155     set syncToolURL(url: string) {
156         this.confStore.localSThg.URL = url;
157         this.save();
158     }
159
160     set syncToolRetry(r: number) {
161         this.confStore.localSThg.retry = r;
162         this.save();
163     }
164
165     set projectsRootDir(p: string) {
166         if (p.charAt(0) === '~') {
167             p = this.confStore.localSThg.tilde + p.substring(1);
168         }
169         this.confStore.projectsRootDir = p;
170         this.save();
171     }
172
173     getLabelRootName(): string {
174         let id = this.confStore.localSThg.ID;
175         if (!id || id === "") {
176             return null;
177         }
178         return id.slice(0, 15);
179     }
180
181     addProject(prj: IProject) {
182         // Substitute tilde with to user home path
183         prj.path = prj.path.trim();
184         if (prj.path.charAt(0) === '~') {
185             prj.path = this.confStore.localSThg.tilde + prj.path.substring(1);
186
187             // Must be a full path (on Linux or Windows)
188         } else if (!((prj.path.charAt(0) === '/') ||
189             (prj.path.charAt(1) === ':' && (prj.path.charAt(2) === '\\' || prj.path.charAt(2) === '/')))) {
190             prj.path = this.confStore.projectsRootDir + '/' + prj.path;
191         }
192
193         if (prj.id == null) {
194             // FIXME - must be done on server side
195             let prefix = this.getLabelRootName() || new Date().toISOString();
196             let splath = prj.path.split('/');
197             prj.id = prefix + "_" + splath[splath.length - 1];
198         }
199
200         if (this._getProjectIdx(prj.id) !== -1) {
201             this.alert.warning("Project already exist (id=" + prj.id + ")", true);
202             return;
203         }
204
205         // TODO - support others project types
206         if (prj.type !== ProjectType.SYNCTHING) {
207             this.alert.error('Project type not supported yet (type: ' + prj.type + ')');
208             return;
209         }
210
211         let sdkPrj: IXDSConfigProject = {
212             id: prj.id,
213             label: prj.label,
214             path: prj.path,
215             hostSyncThingID: this.confStore.localSThg.ID,
216         };
217
218         // Send config to XDS server
219         let newPrj = prj;
220         this.sdkSvr.addProject(sdkPrj)
221             .subscribe(resStRemotePrj => {
222                 newPrj.remotePrjDef = resStRemotePrj;
223
224                 // FIXME REWORK local ST config
225                 //  move logic to server side tunneling-back by WS
226
227                 // Now setup local config
228                 let stLocPrj: ISyncThingProject = {
229                     id: sdkPrj.id,
230                     label: sdkPrj.label,
231                     path: sdkPrj.path,
232                     remoteSyncThingID: resStRemotePrj.builderSThgID
233                 };
234
235                 // Set local Syncthing config
236                 this.stSvr.addProject(stLocPrj)
237                     .subscribe(resStLocalPrj => {
238                         newPrj.localPrjDef = resStLocalPrj;
239
240                         // FIXME: maybe reduce subject to only .project
241                         //this.confSubject.next(Object.assign({}, this.confStore).project);
242                         this.confStore.projects.push(Object.assign({}, newPrj));
243                         this.confSubject.next(Object.assign({}, this.confStore));
244                     },
245                     err => {
246                         this.alert.error("Configuration local ERROR: " + err);
247                     });
248             },
249             err => {
250                 this.alert.error("Configuration remote ERROR: " + err);
251             });
252     }
253
254     deleteProject(prj: IProject) {
255         let idx = this._getProjectIdx(prj.id);
256         if (idx === -1) {
257             throw new Error("Invalid project id (id=" + prj.id + ")");
258         }
259         this.sdkSvr.deleteProject(prj.id)
260             .subscribe(res => {
261                 this.stSvr.deleteProject(prj.id)
262                     .subscribe(res => {
263                         this.confStore.projects.splice(idx, 1);
264                     }, err => {
265                         this.alert.error("Delete local ERROR: " + err);
266                     });
267             }, err => {
268                 this.alert.error("Delete remote ERROR: " + err);
269             });
270     }
271
272     private _getProjectIdx(id: string): number {
273         return this.confStore.projects.findIndex((item) => item.id === id);
274     }
275
276 }