Skip to content

Commit

Permalink
dropbox implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ebrehault committed Jan 10, 2024
1 parent 53bd873 commit 76fce00
Show file tree
Hide file tree
Showing 3 changed files with 372 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { Observable, catchError, concatMap, forkJoin, from, map, of } from 'rxjs';

import { ConnectorParameters, FileStatus, IConnector, Link, SearchResults, SyncItem } from '../../domain/connector';
import { SourceConnectorDefinition } from '../factory';
import { OAuthBaseConnector } from './oauth.base';

export const DropboxConnector: SourceConnectorDefinition = {
id: 'dropbox',
factory: () => new DropboxImpl(),
};

export class DropboxImpl extends OAuthBaseConnector implements IConnector {
params: ConnectorParameters = {};
isExternal = false;

hasAuthData() {
return !!this.params.token;
}

setParameters(params: ConnectorParameters) {
this.params = params;
}

getParameters(): ConnectorParameters {
return this.params;
}

areParametersValid(params: ConnectorParameters) {
if (!params?.token) {
return false;
}
if (!params?.refresh) {
return false;
}
return true;
}

getLastModified(since: string, folders?: SyncItem[] | undefined): Observable<SearchResults> {
if ((folders ?? []).length === 0) {
return of({
items: [],
});
}
try {
return forkJoin((folders || []).map((folder) => this._getFiles('', false, folder.originalId))).pipe(
map((results) => {
const items = results.reduce(
(acc, result) => acc.concat(result.items.filter((item) => item.modifiedGMT && item.modifiedGMT > since)),
[] as SyncItem[],
);
return {
items,
};
}),
);
} catch (err) {
return of({
items: [],
});
}
}

getFilesFromFolders(folders: SyncItem[]): Observable<SearchResults> {
if ((folders ?? []).length === 0) {
return of({
items: [],
});
}
try {
return forkJoin((folders || []).map((folder) => this._getFiles('', false, folder.originalId))).pipe(
map((results) => {
const result: { items: SyncItem[] } = {
items: [],
};
results.forEach((res) => {
result.items = [...result.items, ...res.items];
});
return result;
}),
);
} catch (err) {
return of({
items: [],
});
}
}

getFolders(query?: string | undefined): Observable<SearchResults> {
return this._getFiles(query, true, '');
}

getFiles(query?: string): Observable<SearchResults> {
return this._getFiles(query);
}

isAccesTokenValid(): Observable<boolean> {
return from(
fetch('https://api.dropboxapi.com/2/users/get_current_account ', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.params.token || ''}`,
},
}).then(
(res) => res.json(),
(err) => {
console.error(`Error fetching about: ${err}`);
throw new Error(err);
},
),
).pipe(
concatMap((res) => {
if (res.error && res.error['.tag'] === 'invalid_access_token') {
return of(false);
}
return of(true);
}),
catchError(() => {
return of(true);
}),
);
}

private _getFiles(
query?: string,
loadFolders = false,
path = '',
nextPage?: string | number,
previous?: SearchResults,
): Observable<SearchResults> {
const success = (url: string) => {
/* eslint-disable @typescript-eslint/no-explicit-any */
return (res: any) => {
if (res.status === 200) {
return res.json();
} else if (res.status === 401) {
console.error(`Unauthorized for ${url}`);
throw new Error('Unauthorized');
} else {
console.error(`Error for ${url}`);
return res.text().then((text: string) => {
throw new Error(text || 'Unknown error');
});
}
};
};
const failure = (url: string) => {
return (err: any) => {
console.error(`Error for ${url}: ${err}`);
throw new Error();
};
};
const url = query
? `https://api.dropboxapi.com/2/files/search_v2${nextPage ? '/continue' : ''}`
: `https://api.dropboxapi.com/2/files/list_folder${nextPage ? '/continue' : ''}`;
const params = query ? { query } : { path, recursive: true, limit: 100, include_media_info: true };
const request = fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.params.token || ''}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(nextPage ? { cursor: nextPage } : params),
}).then(success(url), failure(url));
return from(request).pipe(
concatMap((result: any) => {
const newItems =
(query
? result.matches?.filter((item: any) => this.filterResults(item, loadFolders)).map(this.mapResults)
: result.entries?.filter((item: any) => this.filterFiles(item, loadFolders)).map(this.mapFiles)) || [];
const items = [...(previous?.items || []), ...newItems];
return result.has_more
? this._getFiles(query, loadFolders, path, result.cursor, { items, nextPage: result.cursor })
: of({ items });
}),
);
}

/* eslint-disable @typescript-eslint/no-explicit-any */
private mapFiles(raw: any): SyncItem {
const isFolder = raw['.tag'] === 'folder';
return {
title: raw.name || '',
originalId: (isFolder ? raw.path_lower : raw.id) || '',
metadata: {},
status: FileStatus.PENDING,
uuid: raw.uuid || '',
modifiedGMT: raw.client_modified,
isFolder,
};
}

/* eslint-disable @typescript-eslint/no-explicit-any */
private mapResults(raw: any): SyncItem {
return {
title: raw.metadata?.metadata?.['name'] || '',
originalId: raw.metadata?.metadata?.['id'] || '',
metadata: {},
status: FileStatus.PENDING,
uuid: raw.metadata?.metadata?.['uuid'] || '',
isFolder: raw.match_type?.['.tag'] === 'folder',
};
}

/* eslint-disable @typescript-eslint/no-explicit-any */
private filterFiles(raw: any, folders = false): boolean {
return folders ? raw?.['.tag'] === 'folder' : raw?.['.tag'] !== 'folder';
}

/* eslint-disable @typescript-eslint/no-explicit-any */
private filterResults(raw: any, folders = false): boolean {
return folders ? raw.match_type?.['.tag'] === 'folder' : raw.match_type?.['.tag'] !== 'folder';
}

getLink(): Observable<Link> {
throw new Error('Method not implemented.');
}

download(resource: SyncItem): Observable<Blob | undefined> {
try {
return new Observable<Blob | undefined>((observer) => {
fetch('https://content.dropboxapi.com/2/files/download', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.params.token || ''}`,
'Dropbox-API-Arg': JSON.stringify({ path: resource.originalId }),
},
})
.then((res) => res.blob())
.then(
(blob) => {
observer.next(blob);
observer.complete();
},
(e) => {
console.error(e);
observer.next(undefined);
observer.complete();
},
);
});
} catch (e) {
console.error(e);
return of(undefined);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { firstValueFrom, of } from 'rxjs';
import { describe, expect, test, vi } from 'vitest';
import { FileStatus } from '../../../domain/connector';
import { DropboxImpl } from '../dropbox.connector';

const data = [
{
uuid: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
title: 'PO6300590983',
originalId: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
modifiedGMT: '2023-11-29T12:49:27.539Z',
metadata: {},
status: 'PENDING',
},
{
uuid: '19QJOCaOY4R8EQZ7VDrmmu2FBkeOlRAxJ',
title: 'PO6300604892',
originalId: '19QJOCaOY4R8EQZ7VDrmmu2FBkeOlRAxJ',
modifiedGMT: '2023-11-27T12:48:06.061Z',
metadata: {},
status: 'PENDING',
},
{
uuid: '1-5mIXJuiLTFxTO4mmVdXGNdf-Da-EzgA',
title: 'PO4550970006',
originalId: '1-5mIXJuiLTFxTO4mmVdXGNdf-Da-EzgA',
modifiedGMT: '2023-11-27T12:46:08.712Z',
metadata: {},
status: 'PENDING',
},
];

const dropboxTest = test.extend({
// eslint-disable-next-line no-empty-pattern
sourceConnector: async ({}, use) => {
const mock = vi.spyOn(DropboxImpl.prototype, '_getFiles').mockImplementation(() => {
return of({
items: data,
nextPage: null,
});
});
await use(new DropboxImpl());
mock.mockRestore();
},
});

describe('Test validate dropbox params', () => {
dropboxTest('Incorrect - Without params', ({ sourceConnector }) => {
expect(sourceConnector.areParametersValid({})).toBe(false);
});

dropboxTest('Incorrect - With wrong params', ({ sourceConnector }) => {
expect(
sourceConnector.areParametersValid({
incorrect: 'test',
}),
).toBe(false);
});

dropboxTest('Incorrect - With wrong params - one valid', ({ sourceConnector }) => {
expect(
sourceConnector.areParametersValid({
incorrect: 'test',
token: 'test',
}),
).toBe(false);
});

dropboxTest('Incorrect - With wrong params - one valid', ({ sourceConnector }) => {
expect(
sourceConnector.areParametersValid({
incorrect: 'test',
refresh: 'test',
}),
).toBe(false);
});

dropboxTest('Incorrect - With empty params', ({ sourceConnector }) => {
expect(
sourceConnector.areParametersValid({
token: '',
refresh: '',
}),
).toBe(false);
});

dropboxTest('Correct - With correct params', ({ sourceConnector }) => {
expect(
sourceConnector.areParametersValid({
token: 'test',
refresh: 'test',
}),
).toBe(true);
});
});

describe('Test last modified', () => {
dropboxTest('Get last modified', async ({ sourceConnector }) => {
const lastModified = await firstValueFrom(
sourceConnector.getLastModified('2023-11-28T00:00:00.000Z', [
{
uuid: 'test_uuid',
title: 'Test folder',
originalId: 'test_uuid',
metadata: {},
status: FileStatus.PENDING,
},
]),
);

expect(lastModified).toEqual({
items: [
{
uuid: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
title: 'PO6300590983',
originalId: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
modifiedGMT: '2023-11-29T12:49:27.539Z',
metadata: {},
status: FileStatus.PENDING,
},
],
});
});
});
2 changes: 2 additions & 0 deletions electron-app/src/logic/connector/infrastructure/factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IConnector } from '../domain/connector';
import { DropboxConnector } from './connectors/dropbox.connector';
import { FolderConnector } from './connectors/folder.connector';
import { GDriveConnector } from './connectors/gdrive.connector';

Expand All @@ -17,6 +18,7 @@ export type CONNECTORS_NAMES = 'folder' | 'gdrive';
const connectors: { [id: string]: SourceConnectorDefinition } = {
folder: FolderConnector,
gdrive: GDriveConnector,
dropbox: DropboxConnector,
};

// TODO: add the dynamic connectors
Expand Down

0 comments on commit 76fce00

Please sign in to comment.