Used patched (my) version of go-socket.io.
[src/xds/xds-agent.git] / webapp / src / app / services / syncthing.service.ts
1 import { Injectable } from '@angular/core';
2 /*
3 import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
4 import { CookieService } from 'ngx-cookie';
5 import { Location } from '@angular/common';
6 import { Observable } from 'rxjs/Observable';
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/observable/of';
14 import 'rxjs/add/observable/timer';
15 import 'rxjs/add/operator/retryWhen';
16
17 export interface ISyncThingProject {
18     id: string;
19     path: string;
20     serverSyncThingID: string;
21     label?: string;
22 }
23
24 export interface ISyncThingStatus {
25     ID: string;
26     baseURL: string;
27     connected: boolean;
28     connectionRetry: number;
29     tilde: string;
30     rawStatus: any;
31 }
32
33 // Private interfaces of Syncthing
34 const ISTCONFIG_VERSION = 20;
35
36 interface ISTFolderDeviceConfiguration {
37     deviceID: string;
38     introducedBy: string;
39 }
40 interface ISTFolderConfiguration {
41     id: string;
42     label: string;
43     path: string;
44     type?: number;
45     devices?: ISTFolderDeviceConfiguration[];
46     rescanIntervalS?: number;
47     ignorePerms?: boolean;
48     autoNormalize?: boolean;
49     minDiskFreePct?: number;
50     versioning?: { type: string; params: string[] };
51     copiers?: number;
52     pullers?: number;
53     hashers?: number;
54     order?: number;
55     ignoreDelete?: boolean;
56     scanProgressIntervalS?: number;
57     pullerSleepS?: number;
58     pullerPauseS?: number;
59     maxConflicts?: number;
60     disableSparseFiles?: boolean;
61     disableTempIndexes?: boolean;
62     fsync?: boolean;
63     paused?: boolean;
64 }
65
66 interface ISTDeviceConfiguration {
67     deviceID: string;
68     name?: string;
69     address?: string[];
70     compression?: string;
71     certName?: string;
72     introducer?: boolean;
73     skipIntroductionRemovals?: boolean;
74     introducedBy?: string;
75     paused?: boolean;
76     allowedNetwork?: string[];
77 }
78
79 interface ISTGuiConfiguration {
80     enabled: boolean;
81     address: string;
82     user?: string;
83     password?: string;
84     useTLS: boolean;
85     apiKey?: string;
86     insecureAdminAccess?: boolean;
87     theme: string;
88     debugging: boolean;
89     insecureSkipHostcheck?: boolean;
90 }
91
92 interface ISTOptionsConfiguration {
93     listenAddresses: string[];
94     globalAnnounceServer: string[];
95     // To be completed ...
96 }
97
98 interface ISTConfiguration {
99     version: number;
100     folders: ISTFolderConfiguration[];
101     devices: ISTDeviceConfiguration[];
102     gui: ISTGuiConfiguration;
103     options: ISTOptionsConfiguration;
104     ignoredDevices: string[];
105 }
106
107 // Default settings
108 const DEFAULT_GUI_PORT = 8384;
109 const DEFAULT_GUI_API_KEY = "1234abcezam";
110 const DEFAULT_RESCAN_INTERV = 0;    // 0: use syncthing-inotify to detect changes
111
112 */
113
114 @Injectable()
115 export class SyncthingService {
116
117     /* SEB A SUP
118     public Status$: Observable<ISyncThingStatus>;
119
120     private baseRestUrl: string;
121     private apikey: string;
122     private localSTID: string;
123     private stCurVersion: number;
124     private connectionMaxRetry: number;
125     private _status: ISyncThingStatus = {
126         ID: null,
127         baseURL: "",
128         connected: false,
129         connectionRetry: 0,
130         tilde: "",
131         rawStatus: null,
132     };
133     private statusSubject = <BehaviorSubject<ISyncThingStatus>>new BehaviorSubject(this._status);
134
135     constructor(private http: Http, private _window: Window, private cookie: CookieService) {
136         this._status.baseURL = 'http://localhost:' + DEFAULT_GUI_PORT;
137         this.baseRestUrl = this._status.baseURL + '/rest';
138         this.apikey = DEFAULT_GUI_API_KEY;
139         this.stCurVersion = -1;
140         this.connectionMaxRetry = 10;   // 10 seconds
141
142         this.Status$ = this.statusSubject.asObservable();
143     }
144
145     connect(retry: number, url?: string): Observable<ISyncThingStatus> {
146         if (url) {
147             this._status.baseURL = url;
148             this.baseRestUrl = this._status.baseURL + '/rest';
149         }
150         this._status.connected = false;
151         this._status.ID = null;
152         this._status.connectionRetry = 0;
153         this.connectionMaxRetry = retry || 3600;   // 1 hour
154         return this.getStatus();
155     }
156
157     getID(): Observable<string> {
158         if (this._status.ID != null) {
159             return Observable.of(this._status.ID);
160         }
161         return this.getStatus().map(sts => sts.ID);
162     }
163
164     getStatus(): Observable<ISyncThingStatus> {
165         return this._get('/system/status')
166             .map((status) => {
167                 this._status.ID = status["myID"];
168                 this._status.tilde = status["tilde"];
169                 console.debug('ST local ID', this._status.ID);
170
171                 this._status.rawStatus = status;
172
173                 return this._status;
174             });
175     }
176
177     getProjects(): Observable<ISTFolderConfiguration[]> {
178         return this._getConfig()
179             .map((conf) => conf.folders);
180     }
181
182     addProject(prj: ISyncThingProject): Observable<ISTFolderConfiguration> {
183         return this.getID()
184             .flatMap(() => this._getConfig())
185             .flatMap((stCfg) => {
186                 let newDevID = prj.serverSyncThingID;
187
188                 // Add new Device if needed
189                 let dev = stCfg.devices.filter(item => item.deviceID === newDevID);
190                 if (dev.length <= 0) {
191                     stCfg.devices.push(
192                         {
193                             deviceID: newDevID,
194                             name: "Builder_" + newDevID.slice(0, 15),
195                             address: ["dynamic"],
196                         }
197                     );
198                 }
199
200                 // Add or update Folder settings
201                 let label = prj.label || "";
202                 let scanInterval = parseInt(this.cookie.get("st-rescanInterval"), 10) || DEFAULT_RESCAN_INTERV;
203                 let folder: ISTFolderConfiguration = {
204                     id: prj.id,
205                     label: label,
206                     path: prj.path,
207                     devices: [{ deviceID: newDevID, introducedBy: "" }],
208                     autoNormalize: true,
209                     rescanIntervalS: scanInterval,
210                 };
211
212                 let idx = stCfg.folders.findIndex(item => item.id === prj.id);
213                 if (idx === -1) {
214                     stCfg.folders.push(folder);
215                 } else {
216                     let newFld = Object.assign({}, stCfg.folders[idx], folder);
217                     stCfg.folders[idx] = newFld;
218                 }
219
220                 // Set new config
221                 return this._setConfig(stCfg);
222             })
223             .flatMap(() => this._getConfig())
224             .map((newConf) => {
225                 let idx = newConf.folders.findIndex(item => item.id === prj.id);
226                 return newConf.folders[idx];
227             });
228     }
229
230     deleteProject(id: string): Observable<ISTFolderConfiguration> {
231         let delPrj: ISTFolderConfiguration;
232         return this._getConfig()
233             .flatMap((conf: ISTConfiguration) => {
234                 let idx = conf.folders.findIndex(item => item.id === id);
235                 if (idx === -1) {
236                     throw new Error("Cannot delete project: not found");
237                 }
238                 delPrj = Object.assign({}, conf.folders[idx]);
239                 conf.folders.splice(idx, 1);
240                 return this._setConfig(conf);
241             })
242             .map(() => delPrj);
243     }
244
245     //
246     // --- Private functions ---
247     //
248     private _getConfig(): Observable<ISTConfiguration> {
249         return this._get('/system/config');
250     }
251
252     private _setConfig(cfg: ISTConfiguration): Observable<any> {
253         return this._post('/system/config', cfg);
254     }
255
256     private _attachAuthHeaders(options?: any) {
257         options = options || {};
258         let headers = options.headers || new Headers();
259         // headers.append('Authorization', 'Basic ' + btoa('username:password'));
260         headers.append('Accept', 'application/json');
261         headers.append('Content-Type', 'application/json');
262         if (this.apikey !== "") {
263             headers.append('X-API-Key', this.apikey);
264
265         }
266         options.headers = headers;
267         return options;
268     }
269
270     private _checkAlive(): Observable<boolean> {
271         if (this._status.connected) {
272             return Observable.of(true);
273         }
274
275         return this.http.get(this.baseRestUrl + '/system/version', this._attachAuthHeaders())
276             .map((r) => this._status.connected = true)
277             .retryWhen((attempts) => {
278                 this._status.connectionRetry = 0;
279                 return attempts.flatMap(error => {
280                     this._status.connected = false;
281                     if (++this._status.connectionRetry >= this.connectionMaxRetry) {
282                         return Observable.throw("Syncthing local daemon not responding (url=" + this._status.baseURL + ")");
283                     } else {
284                         return Observable.timer(1000);
285                     }
286                 });
287             });
288     }
289
290     private _getAPIVersion(): Observable<number> {
291         if (this.stCurVersion !== -1) {
292             return Observable.of(this.stCurVersion);
293         }
294
295         return this.http.get(this.baseRestUrl + '/system/config', this._attachAuthHeaders())
296             .map((res: Response) => {
297                 let conf: ISTConfiguration = res.json();
298                 this.stCurVersion = (conf && conf.version) || -1;
299                 return this.stCurVersion;
300             })
301             .catch(this._handleError);
302     }
303
304     private _checkAPIVersion(): Observable<number> {
305         return this._getAPIVersion().map(ver => {
306             if (ver !== ISTCONFIG_VERSION) {
307                 throw new Error("Unsupported Syncthing version api (" + ver +
308                     " != " + ISTCONFIG_VERSION + ") !");
309             }
310             return ver;
311         });
312     }
313
314     private _get(url: string): Observable<any> {
315         return this._checkAlive()
316             .flatMap(() => this._checkAPIVersion())
317             .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders()))
318             .map((res: Response) => res.json())
319             .catch(this._handleError);
320     }
321
322     private _post(url: string, body: any): Observable<any> {
323         return this._checkAlive()
324             .flatMap(() => this._checkAPIVersion())
325             .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders()))
326             .map((res: Response) => {
327                 if (res && res.status && res.status === 200) {
328                     return res;
329                 }
330                 throw new Error(res.toString());
331
332             })
333             .catch(this._handleError);
334     }
335
336     private _handleError(error: Response | any) {
337         // In a real world app, you might use a remote logging infrastructure
338         let errMsg: string;
339         if (this._status) {
340             this._status.connected = false;
341         }
342         if (error instanceof Response) {
343             const body = error.json() || 'Server error';
344             const err = body.error || JSON.stringify(body);
345             errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
346         } else {
347             errMsg = error.message ? error.message : error.toString();
348         }
349         return Observable.throw(errMsg);
350     }
351     */
352 }