Skip to content

Commit

Permalink
OneDrive connector (#487)
Browse files Browse the repository at this point in the history
* OneDrive connector

* Update apps/desktop/src/app/upload/select-files/select-files.component.ts

Co-authored-by: Mat Pellerin <[email protected]>

Co-authored-by: Mat Pellerin <[email protected]>
  • Loading branch information
ebrehault and mpellerin42 authored Jan 6, 2023
1 parent 6ae0bfd commit 52c65d1
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 9 deletions.
5 changes: 3 additions & 2 deletions apps/desktop/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BackendConfigurationService, SDKService, UserService } from '@flaps/cor
import { STFUtils, STFTrackingService } from '@flaps/core';
import { TranslateService } from '@ngx-translate/core';
import { SOURCE_ID_KEY } from './sync/models';
import { getDeeplink } from './utils';

@Component({
selector: 'nde-root',
Expand Down Expand Up @@ -55,7 +56,7 @@ export class AppComponent implements OnInit {
this.sdk.nuclia.auth.isAuthenticated().subscribe((isAuthenticated) => {
if (!isAuthenticated) {
const interval = setInterval(() => {
const deeplink = (window as any)['deeplink'] || location.search;
const deeplink = getDeeplink();
if (!this.sdk.nuclia.auth.getToken()) {
if (deeplink && deeplink.includes('?')) {
const querystring = new URLSearchParams(deeplink.split('?')[1]);
Expand All @@ -81,7 +82,7 @@ export class AppComponent implements OnInit {
}, 500);
} else if (localStorage.getItem(SOURCE_ID_KEY)) {
const interval = setInterval(() => {
const deeplink = (window as any)['deeplink'] || location.search;
const deeplink = getDeeplink();
if (deeplink && deeplink.includes('?')) {
if ((window as any)['electron']) {
this.router.navigate(['/add-upload']);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/sync/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ISourceConnector {
getFiles(query?: string, pageSize?: number): Observable<SearchResults>;
download(resource: SyncItem): Observable<Blob>;
getLink?(resource: SyncItem): Observable<{ uri: string; extra_headers: { [key: string]: string } }>;
isAuthError?: (message: any) => boolean;
}

export enum FileStatus {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/app/sync/sources/dropbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../models';
import { BehaviorSubject, filter, from, map, Observable, of, switchMap, switchMapTo, take } from 'rxjs';
import { injectScript } from '@flaps/core';
import { getDeeplink } from '../../utils';

declare var Dropbox: any;

Expand Down Expand Up @@ -76,7 +77,7 @@ class DropboxImpl implements ISourceConnector {
this.dbxAuth.setCodeVerifier(localStorage.getItem(DROPBOX_VERIFIER_CODE_KEY));
}
const interval = setInterval(() => {
const deeplink = (window as any)['deeplink'] || location.search;
const deeplink = getDeeplink();
if (deeplink && deeplink.includes('?')) {
const code = new URLSearchParams(deeplink.split('?')[1]).get('code');
clearInterval(interval);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/app/sync/sources/google.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ConnectorSettings } from '../models';
import { BehaviorSubject, Observable } from 'rxjs';
import { injectScript } from '@flaps/core';
import { environment } from '../../../environments/environment';
import { getDeeplink } from '../../utils';

declare var gapi: any;

Expand Down Expand Up @@ -56,7 +57,7 @@ export class GoogleBaseImpl {
discoveryDocs: this.DISCOVERY_DOCS,
});
const interval = setInterval(() => {
const deeplink = (window as any)['deeplink'] || location.search;
const deeplink = getDeeplink();
if (deeplink && deeplink.includes('?')) {
const params = new URLSearchParams(deeplink.split('?')[1]);
const isGoogle = params.get('google');
Expand Down
159 changes: 159 additions & 0 deletions apps/desktop/src/app/sync/sources/onedrive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {
ConnectorSettings,
ISourceConnector,
SourceConnectorDefinition,
SyncItem,
SearchResults,
ConnectorParameters,
FileStatus,
Field,
} from '../models';
import { Observable, of, from, map, BehaviorSubject, filter, switchMap } from 'rxjs';
import { getDeeplink } from '../../utils';

const SCOPE = 'https://graph.microsoft.com/files.read offline_access';

export const OneDriveConnector: SourceConnectorDefinition = {
id: 'onedrive',
title: 'One Drive',
logo: 'assets/logos/onedrive.svg',
description: 'Microsoft OneDrive file hosting service',
factory: (data?: ConnectorSettings) => of(new OneDriveImpl(data)),
};

const CLIENT_ID = 'ONEDRIVE_CLIENT_ID';
const CLIENT_SECRET = 'ONEDRIVE_CLIENT_SECRET';
const TOKEN = 'ONEDRIVE_TOKEN';

class OneDriveImpl implements ISourceConnector {
hasServerSideAuth = true;
isExternal = true;
resumable = false;
isAuthError = (error: any) => error.code === 'InvalidAuthenticationToken';
private isAuthenticated: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

constructor(data?: ConnectorSettings) {
// eslint-disable-next-line no-empty-function
}

getParameters(): Observable<Field[]> {
return of([
{
id: 'client_id',
label: 'Client id',
type: 'text',
required: true,
},
{
id: 'client_secret',
label: 'Client secret',
type: 'text',
required: true,
},
]);
}

handleParameters(params: ConnectorParameters) {
localStorage.setItem(CLIENT_ID, params.client_id);
localStorage.setItem(CLIENT_SECRET, params.client_secret);
}

goToOAuth(reset?: boolean) {
if (reset) {
localStorage.removeItem(TOKEN);
}
const token = localStorage.getItem(TOKEN);
if (!token) {
if ((window as any)['electron']) {
(window as any)['electron'].openExternal(
`https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${localStorage.getItem(
CLIENT_ID,
)}&scope=${SCOPE}
&response_type=token&redirect_uri=nuclia-desktop://index.html`,
);
} else {
location.href = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${localStorage.getItem(
CLIENT_ID,
)}&scope=${SCOPE}
&response_type=token&redirect_uri=http://localhost:4200`;
}
} else {
this.isAuthenticated.next(true);
}
}

authenticate(): Observable<boolean> {
if (!this.isAuthenticated.getValue()) {
const interval = setInterval(() => {
const deeplink = getDeeplink();
if (deeplink && deeplink.includes('?')) {
const params = new URLSearchParams(deeplink.split('?')[1]);
const token = params.get('access_token') || '';
clearInterval(interval);
if (token) {
localStorage.setItem(TOKEN, token);
this.isAuthenticated.next(true);
}
}
}, 500);
}
return this.isAuthenticated.asObservable();
}

getFiles(query?: string, pageSize = 50, nextPage?: string): Observable<SearchResults> {
let path = `https://graph.microsoft.com/v1.0/me/drive/root`;
if (query) {
path += `/search(q='${query}')`;
} else {
path += `/children`;
}
path += `?top=${pageSize}&filter=file ne null`;
if (nextPage) {
path += `&$skiptoken=${nextPage}`;
}
return this.isAuthenticated.pipe(
filter((isAuth) => isAuth),
switchMap(() =>
from(
fetch(path, {
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN)}`,
},
}).then((res) => res.json()),
),
),
map((res) => {
if (res.error) {
throw res.error;
} else {
const nextPage =
res['@odata.nextLink'] && res['@odata.nextLink'].includes('&$skiptoken=')
? res?.['@odata.nextLink'].split('&$skiptoken=')[1].split('&')[0]
: undefined;
return {
items: (res.value || []).map((item: any) => this.mapToSyncItem(item)),
nextPage,
};
}
}),
);
}

private mapToSyncItem(item: any): SyncItem {
return {
uuid: item.id,
title: item.name,
originalId: item.id,
metadata: { mimeType: item.file.mimeType, downloadLink: item['@microsoft.graph.downloadUrl'] },
status: FileStatus.PENDING,
};
}

getLink(resource: SyncItem): Observable<{ uri: string; extra_headers: { [key: string]: string } }> {
return of({ uri: resource.metadata.downloadLink, extra_headers: {} });
}

download(resource: SyncItem): Observable<Blob> {
throw 'Error';
}
}
4 changes: 3 additions & 1 deletion apps/desktop/src/app/sync/sync.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { S3Connector } from './sources/s3';
import { ProcessingPullResponse } from '@nuclia/core';
import { convertDataURIToBinary, NucliaProtobufConverter } from './protobuf';
import { GCSConnector } from './sources/gcs';
import { OneDriveConnector } from './sources/onedrive';

const ACCOUNT_KEY = 'NUCLIA_ACCOUNT';
const QUEUE_KEY = 'NUCLIA_QUEUE';
Expand Down Expand Up @@ -66,7 +67,8 @@ export class SyncService {
};
} = {
gdrive: { definition: GDrive, settings: environment.connectors.google },
dropbox: { definition: DropboxConnector, settings: environment.connectors.dropbox },
onedrive: { definition: OneDriveConnector, settings: environment.connectors.google },
dropbox: { definition: DropboxConnector, settings: {} },
folder: { definition: FolderConnector, settings: {} },
s3: { definition: S3Connector, settings: {} },
gcs: { definition: GCSConnector, settings: environment.connectors.google },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from 'rxjs/operators';
import { SyncItem, ISourceConnector, SearchResults, SOURCE_ID_KEY } from '../../sync/models';

const defaultAuthCheck = (error: any) => error.message === 'Unauthorized' || error.status === 403;
@Component({
selector: 'nde-select-files',
templateUrl: './select-files.component.html',
Expand Down Expand Up @@ -60,12 +61,12 @@ export class SelectFilesComponent implements AfterViewInit, OnDestroy {
concat(
(this.source as ISourceConnector).getFiles(this.query).pipe(
catchError((error) => {
if (this.source && (error.message = 'Unauthorized' || error.status === 403)) {
if (this.source && (this.source.isAuthError || defaultAuthCheck(error))) {
localStorage.setItem(SOURCE_ID_KEY, this.sourceId || '');
if (this.source.hasServerSideAuth) {
this.source.goToOAuth(true);
}
return this.source.authenticate().pipe(mapTo({ items: [], nextPage: undefined }));
return this.source.authenticate().pipe(mapTo({ items: [], nextPage: undefined } as SearchResults));
} else {
return of({ items: [], nextPage: undefined });
}
Expand All @@ -76,7 +77,7 @@ export class SelectFilesComponent implements AfterViewInit, OnDestroy {
tap(() => {
this.loading = true;
}),
concatMap(() => (this.nextPage ? this.nextPage : of({ items: [], nextPage: undefined }))),
concatMap(() => (this.nextPage ? this.nextPage : of({ items: [], nextPage: undefined } as SearchResults))),
),
).pipe(
tap((res) => {
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/app/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function getDeeplink(): string {
let deeplink = (window as any)['deeplink'] || location.search;
if (!deeplink && location.href.includes('#')) {
deeplink = '?' + location.href.split('#')[1];
}
if (deeplink && deeplink.includes('#') && !deeplink.includes('?')) {
deeplink = deeplink.replace('#', '?');
}
return deeplink;
}
1 change: 1 addition & 0 deletions apps/desktop/src/assets/logos/onedrive.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nuclia",
"version": "1.0.18",
"version": "1.0.19",
"license": "MIT",
"author": "Nuclia.cloud",
"description": "Nuclia frontend apps and libs",
Expand Down

0 comments on commit 52c65d1

Please sign in to comment.