Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
feat: add local file syncing support
Browse files Browse the repository at this point in the history
  • Loading branch information
oae committed Oct 30, 2020
1 parent df08e77 commit ac1127e
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 210 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ ln -s "$PWD/dist" "$HOME/.local/share/gnome-shell/extensions/extensions-sync@elh

## Usage

- You can select the data types that are going to be uploaded in the settings.
- You can select the data types that are going to be saved in the settings.

## For Github

Expand All @@ -47,6 +47,10 @@ ln -s "$PWD/dist" "$HOME/.local/share/gnome-shell/extensions/extensions-sync@elh
2. Create a new token from [here](https://gitlab.com/profile/personal_access_tokens). Only api scope is needed.
3. Open extension settings, select the `Gitlab` provider and fill snippet id from first step and user token from second step.

## For Local

1. Select a file that has read/write permission by your active user. (default backup file is in `~/.config/extensions-sync.json`)

## Development

- This extension is written in Typescript and uses webpack to compile it into javascript.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<enum id="org.gnome.shell.extensions.extensions-sync.provider">
<value nick="Github" value="0" />
<value nick="Gitlab" value="1" />
<value nick="Local" value="2" />
</enum>

<flags id="org.gnome.shell.extensions.extensions-sync.data-provider">
Expand Down Expand Up @@ -32,6 +33,11 @@
<summary>Gitlab token</summary>
<description>Gitlab token</description>
</key>
<key type="s" name="backup-file-location">
<default>""</default>
<summary>Backup file location</summary>
<description>Backup file location</description>
</key>
<key name="provider" enum="org.gnome.shell.extensions.extensions-sync.provider">
<default>'Github'</default>
<summary>Provider for synchronization.</summary>
Expand Down
333 changes: 198 additions & 135 deletions resources/ui/prefs.glade

Large diffs are not rendered by default.

69 changes: 39 additions & 30 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { getCurrentExtensionSettings, notify } from '@esync/shell';
import { logger } from '@esync/utils';
import { Settings } from '@imports/Gio-2.0';
import { EventEmitter } from 'events';
import { ApiProvider, ApiEvent, ApiOperationStatus, ApiProviderType } from './types';
import { Local } from './providers/local';
import { SyncProvider, SyncEvent, SyncOperationStatus, SyncProviderType } from './types';

const debug = logger('api');

export class Api {
private provider: ApiProvider;
private provider: SyncProvider;
private eventEmitter: EventEmitter;
private settings: Settings;
private data: Data;
Expand All @@ -20,52 +21,54 @@ export class Api {
this.settings = getCurrentExtensionSettings();
this.provider = this.createProvider();
this.eventEmitter = eventEmitter;
this.eventEmitter.on(ApiEvent.UPLOAD, this.upload.bind(this));
this.eventEmitter.on(ApiEvent.DOWNLOAD, this.download.bind(this));
this.eventEmitter.on(SyncEvent.SAVE, this.save.bind(this));
this.eventEmitter.on(SyncEvent.READ, this.read.bind(this));
this.settings.connect('changed', this.updateProvider.bind(this));
}

async upload(): Promise<void> {
debug('got upload request, uploading settings...');
async save(): Promise<void> {
debug('got save request, saving settings...');
try {
const status: ApiOperationStatus = await this.provider.upload({
const status: SyncOperationStatus = await this.provider.save({
...(await this.data.getSyncData()),
});
if (status === ApiOperationStatus.FAIL) {
throw new Error('Could not upload');
if (status === SyncOperationStatus.FAIL) {
throw new Error('Could not save');
}
debug(`uploaded settings to ${this.provider.getName()} successfully`);
this.eventEmitter.emit(ApiEvent.UPLOAD_FINISHED, status);
notify(_(`Settings successfully uploaded to ${this.provider.getName()}`));
debug(`saved settings to ${this.provider.getName()} successfully`);
this.eventEmitter.emit(SyncEvent.SAVE_FINISHED, status);
notify(_(`Settings successfully saved to ${this.provider.getName()}`));
} catch (ex) {
this.eventEmitter.emit(ApiEvent.UPLOAD_FINISHED, undefined, ex);
notify(_(`Error occured while uploading settings to ${this.provider.getName()}. Please check the logs.`));
debug(`error occured during upload ${ex}`);
this.eventEmitter.emit(SyncEvent.SAVE_FINISHED, undefined, ex);
notify(_(`Error occured while saving settings to ${this.provider.getName()}. Please check the logs.`));
debug(`error occured during save. -> ${ex}`);
}
}

async download(): Promise<void> {
debug('got download request, downloading settings...');
async read(): Promise<void> {
debug('got read request, reading settings...');
try {
const result: SyncData = await this.provider.download();
debug(`downloaded settings from ${this.provider.getName()} successfully`);
this.eventEmitter.emit(ApiEvent.DOWNLOAD_FINISHED, result);
const result: SyncData = await this.provider.read();
debug(`read settings from ${this.provider.getName()} successfully`);
this.eventEmitter.emit(SyncEvent.READ_FINISHED, result);
} catch (ex) {
this.eventEmitter.emit(ApiEvent.DOWNLOAD_FINISHED, undefined, ex);
notify(_(`Error occured while downloading settings from ${this.provider.getName()}. Please check the logs.`));
debug(`error occured during download ${ex}`);
this.eventEmitter.emit(SyncEvent.READ_FINISHED, undefined, ex);
notify(_(`Error occured while reading settings from ${this.provider.getName()}. Please check the logs.`));
debug(`error occured during read. -> ${ex}`);
}
}

private createProvider(): ApiProvider {
const providerType = this.settings.get_enum('provider') as ApiProviderType;
debug(`changing provider to ${ApiProviderType[providerType]}`);
private createProvider(): SyncProvider {
const providerType = this.settings.get_enum('provider') as SyncProviderType;
debug(`changing provider to ${SyncProviderType[providerType]}`);

switch (providerType) {
case ApiProviderType.GITHUB:
case SyncProviderType.GITHUB:
return this.createGithubProvider();
case ApiProviderType.GITLAB:
case SyncProviderType.GITLAB:
return this.createGitlabProvider();
case SyncProviderType.LOCAL:
return this.createLocalProvider();
default:
return this.createGithubProvider();
}
Expand All @@ -75,17 +78,23 @@ export class Api {
this.provider = this.createProvider();
}

private createGithubProvider(): ApiProvider {
private createGithubProvider(): SyncProvider {
const gistId = this.settings.get_string('github-gist-id');
const userToken = this.settings.get_string('github-user-token');

return new Github(gistId, userToken);
}

private createGitlabProvider(): ApiProvider {
private createGitlabProvider(): SyncProvider {
const snippetId = this.settings.get_string('gitlab-snippet-id');
const userToken = this.settings.get_string('gitlab-user-token');

return new Gitlab(snippetId, userToken);
}

private createLocalProvider(): SyncProvider {
const backupfileLocation = this.settings.get_string('backup-file-location');

return new Local(backupfileLocation);
}
}
20 changes: 14 additions & 6 deletions src/api/providers/github.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { SyncData } from '@esync/data';
import { logger } from '@esync/utils';
import { Context as request } from 'grest/src/app/Context/Context';
import { ApiOperationStatus, ApiProvider } from '../types';
import { SyncOperationStatus, SyncProvider } from '../types';

const debug = logger('github');

export class Github implements ApiProvider {
export class Github implements SyncProvider {
private static GIST_API_URL = 'https://api.github.com/gists';

private gistId: string;
Expand All @@ -16,7 +16,7 @@ export class Github implements ApiProvider {
this.userToken = userToken;
}

async upload(syncData: SyncData): Promise<ApiOperationStatus> {
async save(syncData: SyncData): Promise<SyncOperationStatus> {
const files = Object.keys(syncData).reduce((acc, key) => {
return {
...acc,
Expand All @@ -38,18 +38,26 @@ export class Github implements ApiProvider {
method: 'PATCH',
});

return status === 200 ? ApiOperationStatus.SUCCESS : ApiOperationStatus.FAIL;
if (status !== 200) {
throw new Error(`failed to save data to ${this.getName()}. Server status: ${status}`);
}

return status === 200 ? SyncOperationStatus.SUCCESS : SyncOperationStatus.FAIL;
}

async download(): Promise<SyncData> {
const { body } = await request.fetch(`${Github.GIST_API_URL}/${this.gistId}`, {
async read(): Promise<SyncData> {
const { body, status } = await request.fetch(`${Github.GIST_API_URL}/${this.gistId}`, {
headers: {
'User-Agent': 'Mozilla/5.0',
Authorization: `token ${this.userToken}`,
},
method: 'GET',
});

if (status !== 200) {
throw new Error(`failed to read data from ${this.getName()}. Server status: ${status}`);
}

const syncData: SyncData = Object.keys(body.files).reduce(
(acc, key) => {
try {
Expand Down
20 changes: 14 additions & 6 deletions src/api/providers/gitlab.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SyncData } from '@esync/data';
import { Context as request } from 'grest/src/app/Context/Context';
import { ApiOperationStatus, ApiProvider } from '../types';
import { SyncOperationStatus, SyncProvider } from '../types';

export class Gitlab implements ApiProvider {
export class Gitlab implements SyncProvider {
private static SNIPPETS_API_URL = 'https://gitlab.com/api/v4/snippets';

private snippetId: string;
Expand All @@ -13,7 +13,7 @@ export class Gitlab implements ApiProvider {
this.userToken = userToken;
}

async upload(syncData: SyncData): Promise<ApiOperationStatus> {
async save(syncData: SyncData): Promise<SyncOperationStatus> {
const { status } = await request.fetch(`${Gitlab.SNIPPETS_API_URL}/${this.snippetId}`, {
headers: {
'User-Agent': 'Mozilla/5.0',
Expand All @@ -27,18 +27,26 @@ export class Gitlab implements ApiProvider {
method: 'PUT',
});

return status === 200 ? ApiOperationStatus.SUCCESS : ApiOperationStatus.FAIL;
if (status !== 200) {
throw new Error(`failed to save data to ${this.getName()}. Server status: ${status}`);
}

return status === 200 ? SyncOperationStatus.SUCCESS : SyncOperationStatus.FAIL;
}

async download(): Promise<SyncData> {
const { body } = await request.fetch(`${Gitlab.SNIPPETS_API_URL}/${this.snippetId}/raw`, {
async read(): Promise<SyncData> {
const { body, status } = await request.fetch(`${Gitlab.SNIPPETS_API_URL}/${this.snippetId}/raw`, {
headers: {
'User-Agent': 'Mozilla/5.0',
'PRIVATE-TOKEN': `${this.userToken}`,
},
method: 'GET',
});

if (status !== 200) {
throw new Error(`failed to read data from ${this.getName()}. Server status: ${status}`);
}

return body;
}

Expand Down
54 changes: 54 additions & 0 deletions src/api/providers/local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { SyncData } from '@esync/data';
import { File, FileCreateFlags } from '@imports/Gio-2.0';
import { SyncOperationStatus, SyncProvider } from '../types';

export class Local implements SyncProvider {
private backupfileLocation: string;

constructor(backupfileLocation: string) {
this.backupfileLocation = backupfileLocation;
}

async save(syncData: SyncData): Promise<SyncOperationStatus> {
if (!this.backupfileLocation) {
throw new Error('Please select a backup file location from preferences');
}
const backupFile = File.new_for_uri(this.backupfileLocation);
if (!backupFile.query_exists(null)) {
throw new Error(`Failed to backup settings. ${this.backupfileLocation} does not exist`);
}

backupFile.replace_contents(
imports.byteArray.fromString(JSON.stringify(syncData)),
null,
false,
FileCreateFlags.REPLACE_DESTINATION,
null,
);

return SyncOperationStatus.SUCCESS;
}

async read(): Promise<SyncData> {
const backupFile = File.new_for_uri(this.backupfileLocation);
if (!backupFile.query_exists(null)) {
throw new Error(`Failed to read settings from backup. ${this.backupfileLocation} does not exist`);
}

const [status, syncDataBytes] = backupFile.load_contents(null);

if (!syncDataBytes.length || !status) {
throw new Error(`Failed to read settings from backup. ${this.backupfileLocation} is corrupted`);
}

try {
return JSON.parse(imports.byteArray.toString(syncDataBytes));
} catch (err) {
throw new Error(`${this.backupfileLocation} is not a json file`);
}
}

getName(): string {
return 'Local';
}
}
21 changes: 11 additions & 10 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { SyncData } from '@esync/data';

export enum ApiOperationStatus {
export enum SyncOperationStatus {
SUCCESS,
FAIL,
}

export enum ApiEvent {
UPLOAD = 'UPLOAD',
UPLOAD_FINISHED = 'UPLOAD_FINISHED',
DOWNLOAD = 'DOWNLOAD',
DOWNLOAD_FINISHED = 'DOWNLOAD_FINISHED',
export enum SyncEvent {
SAVE = 'SAVE',
SAVE_FINISHED = 'SAVE_FINISHED',
READ = 'READ',
READ_FINISHED = 'READ_FINISHED',
}

export enum ApiProviderType {
export enum SyncProviderType {
GITHUB,
GITLAB,
LOCAL,
}

export interface ApiProvider {
upload(syncData: SyncData): Promise<ApiOperationStatus>;
download(): Promise<SyncData>;
export interface SyncProvider {
save(syncData: SyncData): Promise<SyncOperationStatus>;
read(): Promise<SyncData>;
getName(): string;
}
Loading

0 comments on commit ac1127e

Please sign in to comment.