X-Git-Url: https://gerrit.automotivelinux.org/gerrit/gitweb?a=blobdiff_plain;f=webapp%2Fsrc%2Fapp%2Fcommon%2Fsyncthing.service.ts;fp=webapp%2Fsrc%2Fapp%2Fcommon%2Fsyncthing.service.ts;h=c8b019329b919e76a0602a09b0c143a5aa1d4a71;hb=ec7051e1da665206f594c7616ad381bfeaea333a;hp=0000000000000000000000000000000000000000;hpb=ca3e1762832b27dc25cf90125b376c56e24e2db2;p=src%2Fxds%2Fxds-server.git diff --git a/webapp/src/app/common/syncthing.service.ts b/webapp/src/app/common/syncthing.service.ts new file mode 100644 index 0000000..c8b0193 --- /dev/null +++ b/webapp/src/app/common/syncthing.service.ts @@ -0,0 +1,342 @@ +import { Injectable } from '@angular/core'; +import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http'; +import { Location } from '@angular/common'; +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +// Import RxJs required methods +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/observable/throw'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/timer'; +import 'rxjs/add/operator/retryWhen'; + +export interface ISyncThingProject { + id: string; + path: string; + remoteSyncThingID: string; + label?: string; +} + +export interface ISyncThingStatus { + ID: string; + baseURL: string; + connected: boolean; + tilde: string; + rawStatus: any; +} + +// Private interfaces of Syncthing +const ISTCONFIG_VERSION = 19; + +interface ISTFolderDeviceConfiguration { + deviceID: string; + introducedBy: string; +} +interface ISTFolderConfiguration { + id: string; + label: string; + path: string; + type?: number; + devices?: ISTFolderDeviceConfiguration[]; + rescanIntervalS?: number; + ignorePerms?: boolean; + autoNormalize?: boolean; + minDiskFreePct?: number; + versioning?: { type: string; params: string[] }; + copiers?: number; + pullers?: number; + hashers?: number; + order?: number; + ignoreDelete?: boolean; + scanProgressIntervalS?: number; + pullerSleepS?: number; + pullerPauseS?: number; + maxConflicts?: number; + disableSparseFiles?: boolean; + disableTempIndexes?: boolean; + fsync?: boolean; + paused?: boolean; +} + +interface ISTDeviceConfiguration { + deviceID: string; + name?: string; + address?: string[]; + compression?: string; + certName?: string; + introducer?: boolean; + skipIntroductionRemovals?: boolean; + introducedBy?: string; + paused?: boolean; + allowedNetwork?: string[]; +} + +interface ISTGuiConfiguration { + enabled: boolean; + address: string; + user?: string; + password?: string; + useTLS: boolean; + apiKey?: string; + insecureAdminAccess?: boolean; + theme: string; + debugging: boolean; + insecureSkipHostcheck?: boolean; +} + +interface ISTOptionsConfiguration { + listenAddresses: string[]; + globalAnnounceServer: string[]; + // To be completed ... +} + +interface ISTConfiguration { + version: number; + folders: ISTFolderConfiguration[]; + devices: ISTDeviceConfiguration[]; + gui: ISTGuiConfiguration; + options: ISTOptionsConfiguration; + ignoredDevices: string[]; +} + +// Default settings +const DEFAULT_GUI_PORT = 8384; +const DEFAULT_GUI_API_KEY = "1234abcezam"; + + +@Injectable() +export class SyncthingService { + + public Status$: Observable; + + private baseRestUrl: string; + private apikey: string; + private localSTID: string; + private stCurVersion: number; + private _status: ISyncThingStatus = { + ID: null, + baseURL: "", + connected: false, + tilde: "", + rawStatus: null, + }; + private statusSubject = >new BehaviorSubject(this._status); + + constructor(private http: Http, private _window: Window) { + this._status.baseURL = 'http://localhost:' + DEFAULT_GUI_PORT; + this.baseRestUrl = this._status.baseURL + '/rest'; + this.apikey = DEFAULT_GUI_API_KEY; + this.stCurVersion = -1; + + this.Status$ = this.statusSubject.asObservable(); + } + + connect(retry: number, url?: string): Observable { + if (url) { + this._status.baseURL = url; + this.baseRestUrl = this._status.baseURL + '/rest'; + } + this._status.connected = false; + this._status.ID = null; + return this.getStatus(retry); + } + + getID(retry?: number): Observable { + if (this._status.ID != null) { + return Observable.of(this._status.ID); + } + return this.getStatus(retry).map(sts => sts.ID); + } + + getStatus(retry?: number): Observable { + + if (retry == null) { + retry = 3600; // 1 hour + } + return this._get('/system/status') + .map((status) => { + this._status.ID = status["myID"]; + this._status.tilde = status["tilde"]; + this._status.connected = true; + console.debug('ST local ID', this._status.ID); + + this._status.rawStatus = status; + + return this._status; + }) + .retryWhen((attempts) => { + let count = 0; + return attempts.flatMap(error => { + if (++count >= retry) { + return this._handleError(error); + } else { + return Observable.timer(count * 1000); + } + }); + }); + } + + getProjects(): Observable { + return this._getConfig() + .map((conf) => conf.folders); + } + + addProject(prj: ISyncThingProject): Observable { + return this.getID() + .flatMap(() => this._getConfig()) + .flatMap((stCfg) => { + let newDevID = prj.remoteSyncThingID; + + // Add new Device if needed + let dev = stCfg.devices.filter(item => item.deviceID === newDevID); + if (dev.length <= 0) { + stCfg.devices.push( + { + deviceID: newDevID, + name: "Builder_" + newDevID.slice(0, 15), + address: ["dynamic"], + } + ); + } + + // Add or update Folder settings + let label = prj.label || ""; + let folder: ISTFolderConfiguration = { + id: prj.id, + label: label, + path: prj.path, + devices: [{ deviceID: newDevID, introducedBy: "" }], + autoNormalize: true, + }; + + let idx = stCfg.folders.findIndex(item => item.id === prj.id); + if (idx === -1) { + stCfg.folders.push(folder); + } else { + let newFld = Object.assign({}, stCfg.folders[idx], folder); + stCfg.folders[idx] = newFld; + } + + // Set new config + return this._setConfig(stCfg); + }) + .flatMap(() => this._getConfig()) + .map((newConf) => { + let idx = newConf.folders.findIndex(item => item.id === prj.id); + return newConf.folders[idx]; + }); + } + + deleteProject(id: string): Observable { + let delPrj: ISTFolderConfiguration; + return this._getConfig() + .flatMap((conf: ISTConfiguration) => { + let idx = conf.folders.findIndex(item => item.id === id); + if (idx === -1) { + throw new Error("Cannot delete project: not found"); + } + delPrj = Object.assign({}, conf.folders[idx]); + conf.folders.splice(idx, 1); + return this._setConfig(conf); + }) + .map(() => delPrj); + } + + /* + * --- Private functions --- + */ + private _getConfig(): Observable { + return this._get('/system/config'); + } + + private _setConfig(cfg: ISTConfiguration): Observable { + return this._post('/system/config', cfg); + } + + private _attachAuthHeaders(options?: any) { + options = options || {}; + let headers = options.headers || new Headers(); + // headers.append('Authorization', 'Basic ' + btoa('username:password')); + headers.append('Accept', 'application/json'); + headers.append('Content-Type', 'application/json'); + if (this.apikey !== "") { + headers.append('X-API-Key', this.apikey); + + } + options.headers = headers; + return options; + } + + private _checkAlive(): Observable { + return this.http.get(this.baseRestUrl + '/system/version', this._attachAuthHeaders()) + .map((r) => this._status.connected = true) + .repeatWhen + .catch((err) => { + this._status.connected = false; + throw new Error("Syncthing local daemon not responding (url=" + this._status.baseURL + ")"); + }); + } + + private _getAPIVersion(): Observable { + if (this.stCurVersion !== -1) { + return Observable.of(this.stCurVersion); + } + + return this._checkAlive() + .flatMap(() => this.http.get(this.baseRestUrl + '/system/config', this._attachAuthHeaders())) + .map((res: Response) => { + let conf: ISTConfiguration = res.json(); + this.stCurVersion = (conf && conf.version) || -1; + return this.stCurVersion; + }) + .catch(this._handleError); + } + + private _checkAPIVersion(): Observable { + return this._getAPIVersion().map(ver => { + if (ver !== ISTCONFIG_VERSION) { + throw new Error("Unsupported Syncthing version api (" + ver + + " != " + ISTCONFIG_VERSION + ") !"); + } + return ver; + }); + } + + private _get(url: string): Observable { + return this._checkAPIVersion() + .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders())) + .map((res: Response) => res.json()) + .catch(this._handleError); + } + + private _post(url: string, body: any): Observable { + return this._checkAPIVersion() + .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders())) + .map((res: Response) => { + if (res && res.status && res.status === 200) { + return res; + } + throw new Error(res.toString()); + + }) + .catch(this._handleError); + } + + private _handleError(error: Response | any) { + // In a real world app, you might use a remote logging infrastructure + let errMsg: string; + if (this._status) { + this._status.connected = false; + } + if (error instanceof Response) { + const body = error.json() || 'Server error'; + const err = body.error || JSON.stringify(body); + errMsg = `${error.status} - ${error.statusText || ''} ${err}`; + } else { + errMsg = error.message ? error.message : error.toString(); + } + return Observable.throw(errMsg); + } +}