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 {
30 // Private interfaces of Syncthing
31 const ISTCONFIG_VERSION = 19;
33 interface ISTFolderDeviceConfiguration {
37 interface ISTFolderConfiguration {
42 devices?: ISTFolderDeviceConfiguration[];
43 rescanIntervalS?: number;
44 ignorePerms?: boolean;
45 autoNormalize?: boolean;
46 minDiskFreePct?: number;
47 versioning?: { type: string; params: string[] };
52 ignoreDelete?: boolean;
53 scanProgressIntervalS?: number;
54 pullerSleepS?: number;
55 pullerPauseS?: number;
56 maxConflicts?: number;
57 disableSparseFiles?: boolean;
58 disableTempIndexes?: boolean;
63 interface ISTDeviceConfiguration {
70 skipIntroductionRemovals?: boolean;
71 introducedBy?: string;
73 allowedNetwork?: string[];
76 interface ISTGuiConfiguration {
83 insecureAdminAccess?: boolean;
86 insecureSkipHostcheck?: boolean;
89 interface ISTOptionsConfiguration {
90 listenAddresses: string[];
91 globalAnnounceServer: string[];
92 // To be completed ...
95 interface ISTConfiguration {
97 folders: ISTFolderConfiguration[];
98 devices: ISTDeviceConfiguration[];
99 gui: ISTGuiConfiguration;
100 options: ISTOptionsConfiguration;
101 ignoredDevices: string[];
105 const DEFAULT_GUI_PORT = 8384;
106 const DEFAULT_GUI_API_KEY = "1234abcezam";
110 export class SyncthingService {
112 public Status$: Observable<ISyncThingStatus>;
114 private baseRestUrl: string;
115 private apikey: string;
116 private localSTID: string;
117 private stCurVersion: number;
118 private _status: ISyncThingStatus = {
125 private statusSubject = <BehaviorSubject<ISyncThingStatus>>new BehaviorSubject(this._status);
127 constructor(private http: Http, private _window: Window) {
128 this._status.baseURL = 'http://localhost:' + DEFAULT_GUI_PORT;
129 this.baseRestUrl = this._status.baseURL + '/rest';
130 this.apikey = DEFAULT_GUI_API_KEY;
131 this.stCurVersion = -1;
133 this.Status$ = this.statusSubject.asObservable();
136 connect(retry: number, url?: string): Observable<ISyncThingStatus> {
138 this._status.baseURL = url;
139 this.baseRestUrl = this._status.baseURL + '/rest';
141 this._status.connected = false;
142 this._status.ID = null;
143 return this.getStatus(retry);
146 getID(retry?: number): Observable<string> {
147 if (this._status.ID != null) {
148 return Observable.of(this._status.ID);
150 return this.getStatus(retry).map(sts => sts.ID);
153 getStatus(retry?: number): Observable<ISyncThingStatus> {
156 retry = 3600; // 1 hour
158 return this._get('/system/status')
160 this._status.ID = status["myID"];
161 this._status.tilde = status["tilde"];
162 this._status.connected = true;
163 console.debug('ST local ID', this._status.ID);
165 this._status.rawStatus = status;
169 .retryWhen((attempts) => {
171 return attempts.flatMap(error => {
172 if (++count >= retry) {
173 return this._handleError(error);
175 return Observable.timer(count * 1000);
181 getProjects(): Observable<ISTFolderConfiguration[]> {
182 return this._getConfig()
183 .map((conf) => conf.folders);
186 addProject(prj: ISyncThingProject): Observable<ISTFolderConfiguration> {
188 .flatMap(() => this._getConfig())
189 .flatMap((stCfg) => {
190 let newDevID = prj.remoteSyncThingID;
192 // Add new Device if needed
193 let dev = stCfg.devices.filter(item => item.deviceID === newDevID);
194 if (dev.length <= 0) {
198 name: "Builder_" + newDevID.slice(0, 15),
199 address: ["dynamic"],
204 // Add or update Folder settings
205 let label = prj.label || "";
206 let folder: ISTFolderConfiguration = {
210 devices: [{ deviceID: newDevID, introducedBy: "" }],
214 let idx = stCfg.folders.findIndex(item => item.id === prj.id);
216 stCfg.folders.push(folder);
218 let newFld = Object.assign({}, stCfg.folders[idx], folder);
219 stCfg.folders[idx] = newFld;
223 return this._setConfig(stCfg);
225 .flatMap(() => this._getConfig())
227 let idx = newConf.folders.findIndex(item => item.id === prj.id);
228 return newConf.folders[idx];
232 deleteProject(id: string): Observable<ISTFolderConfiguration> {
233 let delPrj: ISTFolderConfiguration;
234 return this._getConfig()
235 .flatMap((conf: ISTConfiguration) => {
236 let idx = conf.folders.findIndex(item => item.id === id);
238 throw new Error("Cannot delete project: not found");
240 delPrj = Object.assign({}, conf.folders[idx]);
241 conf.folders.splice(idx, 1);
242 return this._setConfig(conf);
248 * --- Private functions ---
250 private _getConfig(): Observable<ISTConfiguration> {
251 return this._get('/system/config');
254 private _setConfig(cfg: ISTConfiguration): Observable<any> {
255 return this._post('/system/config', cfg);
258 private _attachAuthHeaders(options?: any) {
259 options = options || {};
260 let headers = options.headers || new Headers();
261 // headers.append('Authorization', 'Basic ' + btoa('username:password'));
262 headers.append('Accept', 'application/json');
263 headers.append('Content-Type', 'application/json');
264 if (this.apikey !== "") {
265 headers.append('X-API-Key', this.apikey);
268 options.headers = headers;
272 private _checkAlive(): Observable<boolean> {
273 return this.http.get(this.baseRestUrl + '/system/version', this._attachAuthHeaders())
274 .map((r) => this._status.connected = true)
277 this._status.connected = false;
278 throw new Error("Syncthing local daemon not responding (url=" + this._status.baseURL + ")");
282 private _getAPIVersion(): Observable<number> {
283 if (this.stCurVersion !== -1) {
284 return Observable.of(this.stCurVersion);
287 return this._checkAlive()
288 .flatMap(() => 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._checkAPIVersion()
309 .flatMap(() => this.http.get(this.baseRestUrl + url, this._attachAuthHeaders()))
310 .map((res: Response) => res.json())
311 .catch(this._handleError);
314 private _post(url: string, body: any): Observable<any> {
315 return this._checkAPIVersion()
316 .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders()))
317 .map((res: Response) => {
318 if (res && res.status && res.status === 200) {
321 throw new Error(res.toString());
324 .catch(this._handleError);
327 private _handleError(error: Response | any) {
328 // In a real world app, you might use a remote logging infrastructure
331 this._status.connected = false;
333 if (error instanceof Response) {
334 const body = error.json() || 'Server error';
335 const err = body.error || JSON.stringify(body);
336 errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
338 errMsg = error.message ? error.message : error.toString();
340 return Observable.throw(errMsg);