Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use cases synchronization #2

Merged
merged 10 commits into from
Dec 22, 2023
37 changes: 36 additions & 1 deletion electron-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@
},
"license": "MIT",
"dependencies": {
"@nuclia/core": "1.9.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"electron-squirrel-startup": "^1.0.0",
"express": "^4.18.2",
"localstorage-polyfill": "^1.0.1",
"mime-types": "^2.1.35",
"rxjs": "^7.8.1",
"typescript": "^5.2.2",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@electron-forge/cli": "^6.4.2",
Expand All @@ -44,6 +48,7 @@
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/mime-types": "^2.1.4",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
Expand Down
3 changes: 3 additions & 0 deletions electron-app/src/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export enum EVENTS {
SYNC_CREATED = 'sync-created',
SYNC_UPDATED = 'sync-updated',
SYNC_DELETED = 'sync-deleted',
START_SYNCHRONIZATION_SYNC_OBJECT = 'start-synchronization-sync-object',
FINISH_SYNCHRONIZATION_SYNC_OBJECT = 'finish-synchronization-sync-object',
FINISH_SYNCHRONIZATION_SINGLE_FILE = 'finish-synchronization-single-file',
}

export type EVENTS_TYPE = (typeof EVENTS)[keyof typeof EVENTS];
Expand Down
25 changes: 15 additions & 10 deletions electron-app/src/logic/connector/domain/connector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Observable } from 'rxjs';
import { z } from 'zod';

export enum FileStatus {
PENDING = 'PENDING',
Expand All @@ -16,15 +17,17 @@ export interface ConnectorParameters {
[key: string]: any;
}

export interface SyncItem {
uuid?: string;
title: string;
originalId: string;
metadata: { [key: string]: string };
status: FileStatus;
modifiedGMT?: string;
isFolder?: boolean;
}
export const SyncItemValidator = z.object({
uuid: z.string().optional(),
title: z.string().min(1, { message: 'Required' }),
originalId: z.string().min(1, { message: 'Required' }),
metadata: z.record(z.string()),
status: z.nativeEnum(FileStatus).optional(),
modifiedGMT: z.string().optional(),
isFolder: z.boolean().optional(),
parents: z.array(z.string()).optional(),
});
export type SyncItem = z.infer<typeof SyncItemValidator>;

export interface SearchResults {
items: SyncItem[];
Expand All @@ -48,10 +51,12 @@ export interface IConnector {
getParameters(): ConnectorParameters;
getFolders(query?: string): Observable<SearchResults>;
getFiles(query?: string): Observable<SearchResults>;
getLastModified(since: string, folders?: SyncItem[]): Observable<SyncItem[]>;
getFilesFromFolders(folders: SyncItem[]): Observable<SearchResults>;
getLastModified(since: string, folders?: SyncItem[]): Observable<SearchResults>;
// we cannot use the TextField from the SDK because we want to keep connectors independant
download(resource: SyncItem): Observable<Blob | { body: string; format?: 'PLAIN' | 'MARKDOWN' | 'HTML' } | undefined>;
getLink(resource: SyncItem): Observable<Link>;
hasAuthData(): boolean;
refreshAuthentication(): Observable<boolean>;
isAccesTokenValid(): Observable<boolean>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,57 @@ class FolderImpl implements IConnector {
return this._getFiles(this.params.path, query);
}

getLastModified(since: string, folders?: SyncItem[]): Observable<SyncItem[]> {
getFilesFromFolders(folders: SyncItem[]): Observable<SearchResults> {
if ((folders ?? []).length === 0) {
return of({
items: [],
});
}
try {
return forkJoin((folders || []).map((folder) => this._getFiles(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: [],
});
}
}

getLastModified(since: string, folders?: SyncItem[]): Observable<SearchResults> {
if ((folders ?? []).length === 0) {
return of({
items: [],
});
}

try {
return forkJoin(
(folders || []).map((folder) =>
this._getFiles(folder.originalId).pipe(
switchMap((results) => this.getFilesModifiedSince(results.items, since)),
),
),
).pipe(map((results) => results.reduce((acc, result) => acc.concat(result), [] as SyncItem[])));
).pipe(
map((results) => {
const items = results.reduce((acc, result) => acc.concat(result), [] as SyncItem[]);
return {
items,
};
}),
);
} catch (err) {
return of([]);
return of({
items: [],
});
}
}

Expand Down Expand Up @@ -129,4 +169,7 @@ class FolderImpl implements IConnector {
refreshAuthentication(): Observable<boolean> {
return of(true);
}
isAccesTokenValid(): Observable<boolean> {
return of(true);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable, concatMap, forkJoin, from, map, of } from 'rxjs';
import { Observable, catchError, concatMap, forkJoin, from, map, of } from 'rxjs';

import { ConnectorParameters, FileStatus, IConnector, Link, SearchResults, SyncItem } from '../../domain/connector';
import { SourceConnectorDefinition } from '../factory';
Expand Down Expand Up @@ -35,18 +35,53 @@ export class GDriveImpl extends OAuthBaseConnector implements IConnector {
return true;
}

getLastModified(since: string, folders?: SyncItem[] | undefined): Observable<SyncItem[]> {
getLastModified(since: string, folders?: SyncItem[] | undefined): Observable<SearchResults> {
if ((folders ?? []).length === 0) {
return of({
items: [],
});
}
try {
return forkJoin((folders || []).map((folder) => this._getItems('', folder.uuid))).pipe(
map((results) => {
return results.reduce(
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([]);
return of({
items: [],
});
}
}

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

Expand All @@ -58,6 +93,33 @@ export class GDriveImpl extends OAuthBaseConnector implements IConnector {
return this._getItems(query);
}

isAccesTokenValid(): Observable<boolean> {
return from(
fetch('https://www.googleapis.com/drive/v3/about?fields=user', {
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.status === 'UNAUTHENTICATED') {
return of(false);
}
return of(true);
}),
catchError(() => {
return of(true);
}),
);
}

// Script create the tree https://gist.github.com/tanaikech/97b336f04c739ae0181a606eab3dff42
private _getItems(
query = '',
folder = '',
Expand All @@ -66,7 +128,7 @@ export class GDriveImpl extends OAuthBaseConnector implements IConnector {
previous?: SearchResults,
): Observable<SearchResults> {
let path =
'https://www.googleapis.com/drive/v3/files?pageSize=50&fields=nextPageToken,files(id,name,mimeType,modifiedTime)';
'https://www.googleapis.com/drive/v3/files?pageSize=50&fields=nextPageToken,files(id,name,mimeType,modifiedTime,parents)';
const allDrives = '&corpora=allDrives&supportsAllDrives=true&includeItemsFromAllDrives=true';
path += allDrives;
if (query) {
Expand Down Expand Up @@ -121,6 +183,7 @@ export class GDriveImpl extends OAuthBaseConnector implements IConnector {
title: item.name,
originalId: item.id,
modifiedGMT: item.modifiedTime,
parents: item.parents,
metadata: {
needsPdfConversion: needsPdfConversion ? 'yes' : 'no',
mimeType: needsPdfConversion ? 'application/pdf' : item.mimeType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,20 @@ describe('Test last modified', () => {
]),
);

expect(lastModified).toEqual([
{
uuid: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
title: 'PO6300590983',
originalId: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
modifiedGMT: '2023-11-29T12:49:27.539Z',
metadata: {
needsPdfConversion: 'yes',
mimeType: 'application/pdf',
expect(lastModified).toEqual({
items: [
{
uuid: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
title: 'PO6300590983',
originalId: '1v8WV_aNM5qB_642saVlPhOkN1xI0NtQo',
modifiedGMT: '2023-11-29T12:49:27.539Z',
metadata: {
needsPdfConversion: 'yes',
mimeType: 'application/pdf',
},
status: FileStatus.PENDING,
},
status: 'PENDING',
},
]);
],
});
});
});
Loading