Skip to content

Commit 83883bd

Browse files
committed
feat(file): add google drive support
1 parent 2fa78ee commit 83883bd

File tree

6 files changed

+217
-12
lines changed

6 files changed

+217
-12
lines changed

src/app/file/actions/file.action.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export enum FileActionTypes {
2525
RemoveFile = '[FILE] Remove File',
2626
RemoveFileSuccess = '[FILE] Remove File Success',
2727
RemoveFileFail = '[FILE] Remove File Fail',
28+
29+
Clear = '[FILE] Clear File Store',
2830
}
2931

3032
/**
@@ -54,16 +56,19 @@ export class GetFilesSuccess implements Action {
5456
*/
5557
export class GetFile implements Action {
5658
public readonly type = FileActionTypes.GetFile;
59+
5760
constructor(public payload: BudgetFile) {}
5861
}
5962

6063
export class GetFileSuccess implements Action {
6164
public readonly type = FileActionTypes.GetFileSuccess;
65+
6266
constructor(public payload: { file: BudgetFile; content: string }) {}
6367
}
6468

6569
export class GetFileFail implements Action {
6670
public readonly type = FileActionTypes.GetFileFail;
71+
6772
constructor(public payload: string) {}
6873
}
6974

@@ -151,6 +156,13 @@ export class RemoveFileFail implements Action {
151156
constructor(public payload: BudgetFile) {}
152157
}
153158

159+
export class Clear implements Action {
160+
public readonly type = FileActionTypes.Clear;
161+
public payload: any;
162+
163+
constructor() {}
164+
}
165+
154166
export type FileActions =
155167
| GetFiles
156168
| GetFilesSuccess
@@ -169,4 +181,5 @@ export type FileActions =
169181
| RenameFileFail
170182
| RemoveFile
171183
| RemoveFileSuccess
172-
| RemoveFileFail;
184+
| RemoveFileFail
185+
| Clear;

src/app/file/reducers/file.reducer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export function reducer(state = initialState, action: FileActions): FileState {
7373
);
7474
}
7575

76+
case FileActionTypes.Clear: {
77+
return initialState;
78+
}
79+
7680
default: {
7781
return state;
7882
}

src/app/file/services/dropbox-file-handler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { HttpClient } from '@angular/common/http';
22
import { Store } from '@ngrx/store';
33
import { Dropbox } from 'dropbox';
4+
import { Observable } from 'rxjs/Observable';
5+
import { of } from 'rxjs/observable/of';
46
import {
57
CreateFileFail,
68
CreateFileSuccess,
@@ -21,6 +23,7 @@ import { FileHandler } from './file-handler.interface';
2123
import FileMetadataReference = DropboxTypes.files.FileMetadataReference;
2224

2325
export class DropboxFileHandler implements FileHandler {
26+
public ready: Observable<boolean> = of(true);
2427
private client: Dropbox;
2528

2629
constructor(private http: HttpClient, private fileStore: Store<FileState>, accessToken: string) {

src/app/file/services/file-handler.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { Observable } from 'rxjs/Observable';
12
import { BudgetFile } from '../models/budget-file.model';
23

34
export interface FileHandler {
5+
ready: Observable<boolean>;
6+
47
list();
58

69
get(file: BudgetFile);

src/app/file/services/file.service.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import { HttpClient } from '@angular/common/http';
22
import { Injectable } from '@angular/core';
33
import { Actions, Effect } from '@ngrx/effects';
44
import { Store } from '@ngrx/store';
5-
import { tap } from 'rxjs/operators';
5+
import { of } from 'rxjs/observable/of';
6+
import { switchMap, takeWhile, tap } from 'rxjs/operators';
7+
import { AuthActionTypes } from '../../auth/actions/auth';
68
import { AuthenticationType } from '../../auth/models/authentication-type.model';
79
import { getAuthenticationInfo } from '../../auth/reducers/auth.selector';
810
import { AuthState } from '../../auth/reducers/auth.state';
9-
import { CreateFile, FileActionTypes, GetFile, GetFiles, RemoveFile, RenameFile, SaveFile } from '../actions/file.action';
11+
import { Clear, CreateFile, FileActionTypes, GetFile, RemoveFile, RenameFile, SaveFile } from '../actions/file.action';
1012
import { BudgetFile } from '../models/budget-file.model';
1113
import { FileState } from '../reducers/file.state';
1214
import { DropboxFileHandler } from './dropbox-file-handler';
1315
import { FileHandler } from './file-handler.interface';
16+
import { GoogleFileHandler } from './google-file-handler';
1417

1518
@Injectable()
1619
export class FileService {
@@ -36,7 +39,13 @@ export class FileService {
3639
.ofType<RenameFile>(FileActionTypes.RenameFile)
3740
.pipe(tap(action => this.rename(action.payload.file, action.payload.newName)));
3841

42+
@Effect()
43+
private logoutEffect = this.actions
44+
.ofType(AuthActionTypes.LogoutSuccess, AuthActionTypes.LogoutFail)
45+
.pipe(switchMap(() => of(new Clear())));
46+
3947
private fileHandler: FileHandler;
48+
private ready: boolean = false;
4049

4150
constructor(
4251
private authStore: Store<AuthState>,
@@ -46,61 +55,76 @@ export class FileService {
4655
) {
4756
this.authStore.select(getAuthenticationInfo).subscribe(authenticationInfo => {
4857
if (authenticationInfo.accessToken) {
49-
if (authenticationInfo.type === AuthenticationType.DROPBOX) {
50-
this.fileHandler = new DropboxFileHandler(this.http, this.fileStore, authenticationInfo.accessToken);
58+
switch (authenticationInfo.type) {
59+
case AuthenticationType.DROPBOX:
60+
this.fileHandler = new DropboxFileHandler(this.http, this.fileStore, authenticationInfo.accessToken);
61+
break;
62+
case AuthenticationType.GOOGLE:
63+
this.fileHandler = new GoogleFileHandler(this.http, this.fileStore, authenticationInfo.accessToken);
64+
break;
65+
}
66+
67+
if (this.fileHandler) {
68+
this.fileHandler.ready.pipe(takeWhile(ready => !ready)).subscribe(null, null, () => {
69+
this.ready = true;
70+
this.fileHandler.list();
71+
});
5172
}
52-
this.fileStore.dispatch(new GetFiles());
5373
} else {
5474
this.fileHandler = undefined;
5575
}
5676
});
5777
}
5878

5979
private list() {
60-
if (!this.fileHandler) {
80+
if (!this.isFileHandlerReady()) {
6181
return;
6282
}
6383

6484
this.fileHandler.list();
6585
}
6686

6787
private get(file: BudgetFile) {
68-
if (!this.fileHandler) {
88+
if (!this.isFileHandlerReady()) {
6989
return;
7090
}
7191

7292
this.fileHandler.get(file);
7393
}
7494

7595
private create(name: string) {
76-
if (!this.fileHandler) {
96+
if (!this.isFileHandlerReady()) {
7797
return;
7898
}
7999

80100
this.fileHandler.create(name);
81101
}
82102

83103
private save(file: BudgetFile, newContent: string) {
84-
if (!this.fileHandler) {
104+
if (!this.isFileHandlerReady()) {
85105
return;
86106
}
87107

88108
this.fileHandler.save(file, newContent);
89109
}
90110

91111
private remove(file: BudgetFile) {
92-
if (!this.fileHandler) {
112+
if (!this.isFileHandlerReady()) {
93113
return;
94114
}
95115

96116
this.fileHandler.remove(file);
97117
}
98118

99119
private rename(file: BudgetFile, newName: string) {
100-
if (!this.fileHandler) {
120+
if (!this.isFileHandlerReady()) {
101121
return;
102122
}
103123

104124
this.fileHandler.rename(file, newName);
105125
}
126+
127+
private isFileHandlerReady() {
128+
return this.fileHandler && this.ready;
129+
}
106130
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { HttpClient, HttpHeaders } from '@angular/common/http';
2+
import { Store } from '@ngrx/store';
3+
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
4+
import { Observable } from 'rxjs/Observable';
5+
import {
6+
CreateFileFail,
7+
CreateFileSuccess,
8+
GetFileFail,
9+
GetFilesSuccess,
10+
GetFileSuccess,
11+
RemoveFileFail,
12+
RemoveFileSuccess,
13+
RenameFileFail,
14+
RenameFileSuccess,
15+
SaveFileFail,
16+
SaveFileSuccess,
17+
} from '../actions/file.action';
18+
import { BudgetFile } from '../models/budget-file.model';
19+
import { FileState } from '../reducers/file.state';
20+
import { FileHandler } from './file-handler.interface';
21+
22+
export class GoogleFileHandler implements FileHandler {
23+
public ready: Observable<boolean>;
24+
private readonly directoryName: string = 'K3 Budget';
25+
private directoryId: string;
26+
private readySubject = new BehaviorSubject<boolean>(false);
27+
28+
constructor(private http: HttpClient, private fileStore: Store<FileState>, private accessToken: string) {
29+
this.ready = this.readySubject.asObservable();
30+
this.getDirectoryId();
31+
}
32+
33+
public async list() {
34+
try {
35+
const result = await this.http
36+
.get<any>(`https://www.googleapis.com/drive/v3/files`, {
37+
params: { pageSize: '999', fields: 'files(id, name)', q: `"${this.directoryId}" in parents` },
38+
headers: this.getHeader(),
39+
})
40+
.toPromise();
41+
42+
this.fileStore.dispatch(
43+
new GetFilesSuccess({
44+
files: result.files.map(entry => {
45+
return { id: entry.id, name: entry.name };
46+
}),
47+
})
48+
);
49+
} catch (err) {
50+
// @TODO: handle error
51+
}
52+
}
53+
54+
public async get(file: BudgetFile) {
55+
try {
56+
const content = await this.http
57+
.get(`https://www.googleapis.com/drive/v3/files/${file.id}`, {
58+
responseType: 'text',
59+
params: { alt: 'media' },
60+
headers: this.getHeader(),
61+
})
62+
.toPromise();
63+
64+
this.fileStore.dispatch(new GetFileSuccess({ file, content }));
65+
} catch (err) {
66+
this.fileStore.dispatch(new GetFileFail('Can not download file'));
67+
}
68+
}
69+
70+
public async create(name: string) {
71+
try {
72+
const file = await this.http
73+
.post<any>(
74+
`https://www.googleapis.com/drive/v3/files`,
75+
{ name, parents: [this.directoryId], fields: 'id' },
76+
{ headers: this.getHeader() }
77+
)
78+
.toPromise();
79+
80+
this.fileStore.dispatch(new CreateFileSuccess(new BudgetFile(file.id, name)));
81+
} catch (err) {
82+
this.fileStore.dispatch(new CreateFileFail(name));
83+
}
84+
}
85+
86+
public async remove(file: BudgetFile) {
87+
try {
88+
await this.http.delete(`https://www.googleapis.com/drive/v3/files/${file.id}`, { headers: this.getHeader() }).toPromise();
89+
90+
this.fileStore.dispatch(new RemoveFileSuccess(file));
91+
} catch (err) {
92+
this.fileStore.dispatch(new RemoveFileFail(file));
93+
}
94+
}
95+
96+
public async rename(file: BudgetFile, newName: string) {
97+
try {
98+
await this.http
99+
.patch(`https://www.googleapis.com/drive/v3/files/${file.id}`, { name: newName }, { headers: this.getHeader() })
100+
.toPromise();
101+
102+
this.fileStore.dispatch(new RenameFileSuccess({ file, newName }));
103+
} catch (err) {
104+
this.fileStore.dispatch(new RenameFileFail({ file, newName }));
105+
}
106+
}
107+
108+
public async save(file: BudgetFile, newContent: string) {
109+
try {
110+
await this.http
111+
.patch(`https://www.googleapis.com/upload/drive/v3/files/${file.id}`, newContent, {
112+
params: { uploadType: 'media' },
113+
headers: this.getHeader(),
114+
})
115+
.toPromise();
116+
117+
this.fileStore.dispatch(new SaveFileSuccess(file));
118+
} catch (err) {
119+
this.fileStore.dispatch(new SaveFileFail(file));
120+
}
121+
}
122+
123+
private async getDirectoryId() {
124+
try {
125+
// check budget directory
126+
const result = await this.http
127+
.get<any>(`https://www.googleapis.com/drive/v3/files`, {
128+
params: { pageSize: '1', fields: 'files(id)', q: `name = "${this.directoryName}" and trashed = false` },
129+
headers: this.getHeader(),
130+
})
131+
.toPromise();
132+
133+
if (result.files.length === 1) {
134+
// found
135+
this.directoryId = result.files[0].id;
136+
} else {
137+
// directory does not exist, create it
138+
const createDirectory = await this.http
139+
.post<any>(
140+
`https://www.googleapis.com/drive/v3/files`,
141+
{ name: this.directoryName, mimeType: 'application/vnd.google-apps.folder', fields: 'id' },
142+
{ headers: this.getHeader() }
143+
)
144+
.toPromise();
145+
146+
this.directoryId = createDirectory.id;
147+
}
148+
149+
this.readySubject.next(true);
150+
} catch (err) {
151+
// @TODO: handle error
152+
}
153+
}
154+
155+
private getHeader() {
156+
return new HttpHeaders().set('Authorization', 'Bearer ' + this.accessToken);
157+
}
158+
}

0 commit comments

Comments
 (0)