1 import { Injectable } from '@angular/core';
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';
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';
17 export interface ISyncThingProject {
20 serverSyncThingID: string;
24 export interface ISyncThingStatus {
28 connectionRetry: number;
33 // Private interfaces of Syncthing
34 const ISTCONFIG_VERSION = 20;
36 interface ISTFolderDeviceConfiguration {
40 interface ISTFolderConfiguration {
45 devices?: ISTFolderDeviceConfiguration[];
46 rescanIntervalS?: number;
47 ignorePerms?: boolean;
48 autoNormalize?: boolean;
49 minDiskFreePct?: number;
50 versioning?: { type: string; params: string[] };
55 ignoreDelete?: boolean;
56 scanProgressIntervalS?: number;
57 pullerSleepS?: number;
58 pullerPauseS?: number;
59 maxConflicts?: number;
60 disableSparseFiles?: boolean;
61 disableTempIndexes?: boolean;
66 interface ISTDeviceConfiguration {
73 skipIntroductionRemovals?: boolean;
74 introducedBy?: string;
76 allowedNetwork?: string[];
79 interface ISTGuiConfiguration {
86 insecureAdminAccess?: boolean;
89 insecureSkipHostcheck?: boolean;
92 interface ISTOptionsConfiguration {
93 listenAddresses: string[];
94 globalAnnounceServer: string[];
95 // To be completed ...
98 interface ISTConfiguration {
100 folders: ISTFolderConfiguration[];
101 devices: ISTDeviceConfiguration[];
102 gui: ISTGuiConfiguration;
103 options: ISTOptionsConfiguration;
104 ignoredDevices: string[];
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
115 export class SyncthingService {
118 public Status$: Observable<ISyncThingStatus>;
120 private baseRestUrl: string;
121 private apikey: string;
122 private localSTID: string;
123 private stCurVersion: number;
124 private connectionMaxRetry: number;
125 private _status: ISyncThingStatus = {
133 private statusSubject = <BehaviorSubject<ISyncThingStatus>>new BehaviorSubject(this._status);
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
142 this.Status$ = this.statusSubject.asObservable();
145 connect(retry: number, url?: string): Observable<ISyncThingStatus> {
147 this._status.baseURL = url;
148 this.baseRestUrl = this._status.baseURL + '/rest';
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();
157 getID(): Observable<string> {
158 if (this._status.ID != null) {
159 return Observable.of(this._status.ID);
161 return this.getStatus().map(sts => sts.ID);
164 getStatus(): Observable<ISyncThingStatus> {
165 return this._get('/system/status')
167 this._status.ID = status["myID"];
168 this._status.tilde = status["tilde"];
169 console.debug('ST local ID', this._status.ID);
171 this._status.rawStatus = status;
177 getProjects(): Observable<ISTFolderConfiguration[]> {
178 return this._getConfig()
179 .map((conf) => conf.folders);
182 addProject(prj: ISyncThingProject): Observable<ISTFolderConfiguration> {
184 .flatMap(() => this._getConfig())
185 .flatMap((stCfg) => {
186 let newDevID = prj.serverSyncThingID;
188 // Add new Device if needed
189 let dev = stCfg.devices.filter(item => item.deviceID === newDevID);
190 if (dev.length <= 0) {
194 name: "Builder_" + newDevID.slice(0, 15),
195 address: ["dynamic"],
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 = {
207 devices: [{ deviceID: newDevID, introducedBy: "" }],
209 rescanIntervalS: scanInterval,
212 let idx = stCfg.folders.findIndex(item => item.id === prj.id);
214 stCfg.folders.push(folder);
216 let newFld = Object.assign({}, stCfg.folders[idx], folder);
217 stCfg.folders[idx] = newFld;
221 return this._setConfig(stCfg);
223 .flatMap(() => this._getConfig())
225 let idx = newConf.folders.findIndex(item => item.id === prj.id);
226 return newConf.folders[idx];
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);
236 throw new Error("Cannot delete project: not found");
238 delPrj = Object.assign({}, conf.folders[idx]);
239 conf.folders.splice(idx, 1);
240 return this._setConfig(conf);
246 // --- Private functions ---
248 private _getConfig(): Observable<ISTConfiguration> {
249 return this._get('/system/config');
252 private _setConfig(cfg: ISTConfiguration): Observable<any> {
253 return this._post('/system/config', cfg);
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);
266 options.headers = headers;
270 private _checkAlive(): Observable<boolean> {
271 if (this._status.connected) {
272 return Observable.of(true);
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 + ")");
284 return Observable.timer(1000);
290 private _getAPIVersion(): Observable<number> {
291 if (this.stCurVersion !== -1) {
292 return Observable.of(this.stCurVersion);
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;
301 .catch(this._handleError);
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 + ") !");
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);
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) {
330 throw new Error(res.toString());
333 .catch(this._handleError);
336 private _handleError(error: Response | any) {
337 // In a real world app, you might use a remote logging infrastructure
340 this._status.connected = false;
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}`;
347 errMsg = error.message ? error.message : error.toString();
349 return Observable.throw(errMsg);