1 import { Injectable } from '@angular/core';
2 import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
3 import { CookieService } from 'ngx-cookie';
4 import { Location } from '@angular/common';
5 import { Observable } from 'rxjs/Observable';
6 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
8 // Import RxJs required methods
9 import 'rxjs/add/operator/map';
10 import 'rxjs/add/operator/catch';
11 import 'rxjs/add/observable/throw';
12 import 'rxjs/add/observable/of';
13 import 'rxjs/add/observable/timer';
14 import 'rxjs/add/operator/retryWhen';
16 export interface ISyncThingProject {
19 serverSyncThingID: string;
23 export interface ISyncThingStatus {
27 connectionRetry: number;
32 // Private interfaces of Syncthing
33 const ISTCONFIG_VERSION = 20;
35 interface ISTFolderDeviceConfiguration {
39 interface ISTFolderConfiguration {
44 devices?: ISTFolderDeviceConfiguration[];
45 rescanIntervalS?: number;
46 ignorePerms?: boolean;
47 autoNormalize?: boolean;
48 minDiskFreePct?: number;
49 versioning?: { type: string; params: string[] };
54 ignoreDelete?: boolean;
55 scanProgressIntervalS?: number;
56 pullerSleepS?: number;
57 pullerPauseS?: number;
58 maxConflicts?: number;
59 disableSparseFiles?: boolean;
60 disableTempIndexes?: boolean;
65 interface ISTDeviceConfiguration {
72 skipIntroductionRemovals?: boolean;
73 introducedBy?: string;
75 allowedNetwork?: string[];
78 interface ISTGuiConfiguration {
85 insecureAdminAccess?: boolean;
88 insecureSkipHostcheck?: boolean;
91 interface ISTOptionsConfiguration {
92 listenAddresses: string[];
93 globalAnnounceServer: string[];
94 // To be completed ...
97 interface ISTConfiguration {
99 folders: ISTFolderConfiguration[];
100 devices: ISTDeviceConfiguration[];
101 gui: ISTGuiConfiguration;
102 options: ISTOptionsConfiguration;
103 ignoredDevices: string[];
107 const DEFAULT_GUI_PORT = 8386;
108 const DEFAULT_GUI_API_KEY = "1234abcezam";
109 const DEFAULT_RESCAN_INTERV = 0; // 0: use syncthing-inotify to detect changes
113 export class SyncthingService {
115 public Status$: Observable<ISyncThingStatus>;
117 private baseRestUrl: string;
118 private apikey: string;
119 private localSTID: string;
120 private stCurVersion: number;
121 private connectionMaxRetry: number;
122 private _status: ISyncThingStatus = {
130 private statusSubject = <BehaviorSubject<ISyncThingStatus>>new BehaviorSubject(this._status);
132 constructor(private http: Http, private _window: Window, private cookie: CookieService) {
133 this._status.baseURL = 'http://localhost:' + DEFAULT_GUI_PORT;
134 this.baseRestUrl = this._status.baseURL + '/rest';
135 this.apikey = DEFAULT_GUI_API_KEY;
136 this.stCurVersion = -1;
137 this.connectionMaxRetry = 10; // 10 seconds
139 this.Status$ = this.statusSubject.asObservable();
142 connect(retry: number, url?: string): Observable<ISyncThingStatus> {
144 this._status.baseURL = url;
145 this.baseRestUrl = this._status.baseURL + '/rest';
147 this._status.connected = false;
148 this._status.ID = null;
149 this._status.connectionRetry = 0;
150 this.connectionMaxRetry = retry || 3600; // 1 hour
151 return this.getStatus();
154 getID(): Observable<string> {
155 if (this._status.ID != null) {
156 return Observable.of(this._status.ID);
158 return this.getStatus().map(sts => sts.ID);
161 getStatus(): Observable<ISyncThingStatus> {
162 return this._get('/system/status')
164 this._status.ID = status["myID"];
165 this._status.tilde = status["tilde"];
166 console.debug('ST local ID', this._status.ID);
168 this._status.rawStatus = status;
174 getProjects(): Observable<ISTFolderConfiguration[]> {
175 return this._getConfig()
176 .map((conf) => conf.folders);
179 addProject(prj: ISyncThingProject): Observable<ISTFolderConfiguration> {
181 .flatMap(() => this._getConfig())
182 .flatMap((stCfg) => {
183 let newDevID = prj.serverSyncThingID;
185 // Add new Device if needed
186 let dev = stCfg.devices.filter(item => item.deviceID === newDevID);
187 if (dev.length <= 0) {
191 name: "Builder_" + newDevID.slice(0, 15),
192 address: ["dynamic"],
197 // Add or update Folder settings
198 let label = prj.label || "";
199 let scanInterval = parseInt(this.cookie.get("st-rescanInterval"), 10) || DEFAULT_RESCAN_INTERV;
200 let folder: ISTFolderConfiguration = {
204 devices: [{ deviceID: newDevID, introducedBy: "" }],
206 rescanIntervalS: scanInterval,
209 let idx = stCfg.folders.findIndex(item => item.id === prj.id);
211 stCfg.folders.push(folder);
213 let newFld = Object.assign({}, stCfg.folders[idx], folder);
214 stCfg.folders[idx] = newFld;
218 return this._setConfig(stCfg);
220 .flatMap(() => this._getConfig())
222 let idx = newConf.folders.findIndex(item => item.id === prj.id);
223 return newConf.folders[idx];
227 deleteProject(id: string): Observable<ISTFolderConfiguration> {
228 let delPrj: ISTFolderConfiguration;
229 return this._getConfig()
230 .flatMap((conf: ISTConfiguration) => {
231 let idx = conf.folders.findIndex(item => item.id === id);
233 throw new Error("Cannot delete project: not found");
235 delPrj = Object.assign({}, conf.folders[idx]);
236 conf.folders.splice(idx, 1);
237 return this._setConfig(conf);
243 * --- Private functions ---
245 private _getConfig(): Observable<ISTConfiguration> {
246 return this._get('/system/config');
249 private _setConfig(cfg: ISTConfiguration): Observable<any> {
250 return this._post('/system/config', cfg);
253 private _attachAuthHeaders(options?: any) {
254 options = options || {};
255 let headers = options.headers || new Headers();
256 // headers.append('Authorization', 'Basic ' + btoa('username:password'));
257 headers.append('Accept', 'application/json');
258 headers.append('Content-Type', 'application/json');
259 if (this.apikey !== "") {
260 headers.append('X-API-Key', this.apikey);
263 options.headers = headers;
267 private _checkAlive(): Observable<boolean> {
268 if (this._status.connected) {
269 return Observable.of(true);
272 return this.http.get(this.baseRestUrl + '/system/version', this._attachAuthHeaders())
273 .map((r) => this._status.connected = true)
274 .retryWhen((attempts) => {
275 this._status.connectionRetry = 0;
276 return attempts.flatMap(error => {
277 this._status.connected = false;
278 if (++this._status.connectionRetry >= this.connectionMaxRetry) {
279 return Observable.throw("Syncthing local daemon not responding (url=" + this._status.baseURL + ")");
281 return Observable.timer(1000);
287 private _getAPIVersion(): Observable<number> {
288 if (this.stCurVersion !== -1) {
289 return Observable.of(this.stCurVersion);
292 return this.http.get(this.baseRestUrl + '/system/config', this._attachAuthHeaders())
293 .map((res: Response) => {
294 let conf: ISTConfiguration = res.json();
295 this.stCurVersion = (conf && conf.version) || -1;
296 return this.stCurVersion;
298 .catch(this._handleError);
301 private _checkAPIVersion(): Observable<number> {
302 return this._getAPIVersion().map(ver => {
303 if (ver !== ISTCONFIG_VERSION) {
304 throw new Error("Unsupported Syncthing version api (" + ver +
305 " != " + ISTCONFIG_VERSION + ") !");
311 private _get(url: string): Observable<any> {
312 return this._checkAlive()
313 .flatMap(() => this._checkAPIVersion())
314 .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders()))
315 .map((res: Response) => res.json())
316 .catch(this._handleError);
319 private _post(url: string, body: any): Observable<any> {
320 return this._checkAlive()
321 .flatMap(() => this._checkAPIVersion())
322 .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders()))
323 .map((res: Response) => {
324 if (res && res.status && res.status === 200) {
327 throw new Error(res.toString());
330 .catch(this._handleError);
333 private _handleError(error: Response | any) {
334 // In a real world app, you might use a remote logging infrastructure
337 this._status.connected = false;
339 if (error instanceof Response) {
340 const body = error.json() || 'Server error';
341 const err = body.error || JSON.stringify(body);
342 errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
344 errMsg = error.message ? error.message : error.toString();
346 return Observable.throw(errMsg);