1 import { Injectable } from '@angular/core';
2 import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
3 import { Location } from '@angular/common';
4 import { Observable } from 'rxjs/Observable';
5 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
7 // Import RxJs required methods
8 import 'rxjs/add/operator/map';
9 import 'rxjs/add/operator/catch';
10 import 'rxjs/add/observable/throw';
11 import 'rxjs/add/observable/of';
12 import 'rxjs/add/observable/timer';
13 import 'rxjs/add/operator/retryWhen';
15 export interface ISyncThingProject {
18 remoteSyncThingID: string;
22 export interface ISyncThingStatus {
26 connectionRetry: number;
31 // Private interfaces of Syncthing
32 const ISTCONFIG_VERSION = 20;
34 interface ISTFolderDeviceConfiguration {
38 interface ISTFolderConfiguration {
43 devices?: ISTFolderDeviceConfiguration[];
44 rescanIntervalS?: number;
45 ignorePerms?: boolean;
46 autoNormalize?: boolean;
47 minDiskFreePct?: number;
48 versioning?: { type: string; params: string[] };
53 ignoreDelete?: boolean;
54 scanProgressIntervalS?: number;
55 pullerSleepS?: number;
56 pullerPauseS?: number;
57 maxConflicts?: number;
58 disableSparseFiles?: boolean;
59 disableTempIndexes?: boolean;
64 interface ISTDeviceConfiguration {
71 skipIntroductionRemovals?: boolean;
72 introducedBy?: string;
74 allowedNetwork?: string[];
77 interface ISTGuiConfiguration {
84 insecureAdminAccess?: boolean;
87 insecureSkipHostcheck?: boolean;
90 interface ISTOptionsConfiguration {
91 listenAddresses: string[];
92 globalAnnounceServer: string[];
93 // To be completed ...
96 interface ISTConfiguration {
98 folders: ISTFolderConfiguration[];
99 devices: ISTDeviceConfiguration[];
100 gui: ISTGuiConfiguration;
101 options: ISTOptionsConfiguration;
102 ignoredDevices: string[];
106 const DEFAULT_GUI_PORT = 8384;
107 const DEFAULT_GUI_API_KEY = "1234abcezam";
111 export class SyncthingService {
113 public Status$: Observable<ISyncThingStatus>;
115 private baseRestUrl: string;
116 private apikey: string;
117 private localSTID: string;
118 private stCurVersion: number;
119 private connectionMaxRetry: number;
120 private _status: ISyncThingStatus = {
128 private statusSubject = <BehaviorSubject<ISyncThingStatus>>new BehaviorSubject(this._status);
130 constructor(private http: Http, private _window: Window) {
131 this._status.baseURL = 'http://localhost:' + DEFAULT_GUI_PORT;
132 this.baseRestUrl = this._status.baseURL + '/rest';
133 this.apikey = DEFAULT_GUI_API_KEY;
134 this.stCurVersion = -1;
135 this.connectionMaxRetry = 10; // 10 seconds
137 this.Status$ = this.statusSubject.asObservable();
140 connect(retry: number, url?: string): Observable<ISyncThingStatus> {
142 this._status.baseURL = url;
143 this.baseRestUrl = this._status.baseURL + '/rest';
145 this._status.connected = false;
146 this._status.ID = null;
147 this._status.connectionRetry = 0;
148 this.connectionMaxRetry = retry || 3600; // 1 hour
149 return this.getStatus();
152 getID(): Observable<string> {
153 if (this._status.ID != null) {
154 return Observable.of(this._status.ID);
156 return this.getStatus().map(sts => sts.ID);
159 getStatus(): Observable<ISyncThingStatus> {
160 return this._get('/system/status')
162 this._status.ID = status["myID"];
163 this._status.tilde = status["tilde"];
164 console.debug('ST local ID', this._status.ID);
166 this._status.rawStatus = status;
172 getProjects(): Observable<ISTFolderConfiguration[]> {
173 return this._getConfig()
174 .map((conf) => conf.folders);
177 addProject(prj: ISyncThingProject): Observable<ISTFolderConfiguration> {
179 .flatMap(() => this._getConfig())
180 .flatMap((stCfg) => {
181 let newDevID = prj.remoteSyncThingID;
183 // Add new Device if needed
184 let dev = stCfg.devices.filter(item => item.deviceID === newDevID);
185 if (dev.length <= 0) {
189 name: "Builder_" + newDevID.slice(0, 15),
190 address: ["dynamic"],
195 // Add or update Folder settings
196 let label = prj.label || "";
197 let folder: ISTFolderConfiguration = {
201 devices: [{ deviceID: newDevID, introducedBy: "" }],
205 let idx = stCfg.folders.findIndex(item => item.id === prj.id);
207 stCfg.folders.push(folder);
209 let newFld = Object.assign({}, stCfg.folders[idx], folder);
210 stCfg.folders[idx] = newFld;
214 return this._setConfig(stCfg);
216 .flatMap(() => this._getConfig())
218 let idx = newConf.folders.findIndex(item => item.id === prj.id);
219 return newConf.folders[idx];
223 deleteProject(id: string): Observable<ISTFolderConfiguration> {
224 let delPrj: ISTFolderConfiguration;
225 return this._getConfig()
226 .flatMap((conf: ISTConfiguration) => {
227 let idx = conf.folders.findIndex(item => item.id === id);
229 throw new Error("Cannot delete project: not found");
231 delPrj = Object.assign({}, conf.folders[idx]);
232 conf.folders.splice(idx, 1);
233 return this._setConfig(conf);
239 * --- Private functions ---
241 private _getConfig(): Observable<ISTConfiguration> {
242 return this._get('/system/config');
245 private _setConfig(cfg: ISTConfiguration): Observable<any> {
246 return this._post('/system/config', cfg);
249 private _attachAuthHeaders(options?: any) {
250 options = options || {};
251 let headers = options.headers || new Headers();
252 // headers.append('Authorization', 'Basic ' + btoa('username:password'));
253 headers.append('Accept', 'application/json');
254 headers.append('Content-Type', 'application/json');
255 if (this.apikey !== "") {
256 headers.append('X-API-Key', this.apikey);
259 options.headers = headers;
263 private _checkAlive(): Observable<boolean> {
264 if (this._status.connected) {
265 return Observable.of(true);
268 return this.http.get(this.baseRestUrl + '/system/version', this._attachAuthHeaders())
269 .map((r) => this._status.connected = true)
270 .retryWhen((attempts) => {
271 this._status.connectionRetry = 0;
272 return attempts.flatMap(error => {
273 this._status.connected = false;
274 if (++this._status.connectionRetry >= this.connectionMaxRetry) {
275 return Observable.throw("Syncthing local daemon not responding (url=" + this._status.baseURL + ")");
277 return Observable.timer(1000);
283 private _getAPIVersion(): Observable<number> {
284 if (this.stCurVersion !== -1) {
285 return Observable.of(this.stCurVersion);
288 return this.http.get(this.baseRestUrl + '/system/config', this._attachAuthHeaders())
289 .map((res: Response) => {
290 let conf: ISTConfiguration = res.json();
291 this.stCurVersion = (conf && conf.version) || -1;
292 return this.stCurVersion;
294 .catch(this._handleError);
297 private _checkAPIVersion(): Observable<number> {
298 return this._getAPIVersion().map(ver => {
299 if (ver !== ISTCONFIG_VERSION) {
300 throw new Error("Unsupported Syncthing version api (" + ver +
301 " != " + ISTCONFIG_VERSION + ") !");
307 private _get(url: string): Observable<any> {
308 return this._checkAlive()
309 .flatMap(() => this._checkAPIVersion())
310 .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders()))
311 .map((res: Response) => res.json())
312 .catch(this._handleError);
315 private _post(url: string, body: any): Observable<any> {
316 return this._checkAlive()
317 .flatMap(() => this._checkAPIVersion())
318 .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders()))
319 .map((res: Response) => {
320 if (res && res.status && res.status === 200) {
323 throw new Error(res.toString());
326 .catch(this._handleError);
329 private _handleError(error: Response | any) {
330 // In a real world app, you might use a remote logging infrastructure
333 this._status.connected = false;
335 if (error instanceof Response) {
336 const body = error.json() || 'Server error';
337 const err = body.error || JSON.stringify(body);
338 errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
340 errMsg = error.message ? error.message : error.toString();
342 return Observable.throw(errMsg);