diff --git a/src/events/consumer.ts b/src/events/consumer.ts deleted file mode 100644 index 7fb3cf0..0000000 --- a/src/events/consumer.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { type Event } from "./types"; -import SyncManager from "src/sync-manager"; - -export default class EventsConsumer { - constructor(private readonly syncManager: SyncManager) {} - - async process(event: Event): Promise { - if (event.type == "create" || event.type == "modify") { - await this.syncManager.uploadFile(event.file); - } else if (event.type == "delete") { - await this.syncManager.deleteFile(event.filePath); - } - } -} diff --git a/src/github/client.ts b/src/github/client.ts index b126e5a..1a59ba5 100644 --- a/src/github/client.ts +++ b/src/github/client.ts @@ -1,5 +1,4 @@ -import { Vault, requestUrl, normalizePath } from "obsidian"; -import MetadataStore, { Metadata } from "../metadata-store"; +import { requestUrl } from "obsidian"; export type RepoContent = { files: { [key: string]: GetTreeResponseItem }; @@ -132,62 +131,4 @@ export default class GithubClient { }); return res.json; } - - /** - * Create or edit a remote file to GitHub. - * - * @param remoteFilePath Path to remote file - * @param fileContent Content of the file - * @param sha SHA of the file - */ - async uploadFile( - remoteFilePath: string, - fileContent: ArrayBuffer, - sha: string | null, - ) { - await requestUrl({ - url: `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${remoteFilePath}`, - method: "PUT", - headers: this.headers(), - body: JSON.stringify({ - message: `Edit ${remoteFilePath}`, - branch: this.branch, - content: Buffer.from(fileContent).toString("base64"), - sha: sha, - }), - }); - } - - /** - * Gets the SHA of a file in the remote repo. - * - * @param remoteFilePath Path to remote file - * @returns sha of the file as string - */ - async getFileSha(remoteFilePath: string) { - const res = await requestUrl({ - url: `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${remoteFilePath}?ref=${this.branch}`, - headers: this.headers(), - }); - return res.json.sha; - } - - /** - * Delete a single file from GitHub. - * - * @param remoteFilePath Path to remote file - * @param sha SHA of the file - */ - async deleteFile(remoteFilePath: string, sha: string) { - await requestUrl({ - url: `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${remoteFilePath}`, - method: "DELETE", - headers: this.headers(), - body: JSON.stringify({ - message: `Delete ${remoteFilePath}`, - branch: this.branch, - sha: sha, - }), - }); - } } diff --git a/src/main.ts b/src/main.ts index 812b25e..d72410f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,6 @@ import { EventRef, Plugin, FileView } from "obsidian"; import { GitHubSyncSettings, DEFAULT_SETTINGS } from "./settings/settings"; import GitHubSyncSettingsTab from "./settings/tab"; -import { UploadDialog } from "./views/upload-all-files-dialog/view"; import SyncManager from "./sync-manager"; export default class GitHubSyncPlugin extends Plugin { @@ -9,10 +8,7 @@ export default class GitHubSyncPlugin extends Plugin { syncManager: SyncManager; statusBarItem: HTMLElement | null = null; - downloadAllRibbonIcon: HTMLElement | null = null; uploadModifiedFilesRibbonIcon: HTMLElement | null = null; - uploadCurrentFileRibbonIcon: HTMLElement | null = null; - uploadAllFilesRibbonIcon: HTMLElement | null = null; activeLeafChangeListener: EventRef | null = null; vaultCreateListener: EventRef | null = null; @@ -56,71 +52,21 @@ export default class GitHubSyncPlugin extends Plugin { this.showStatusBarItem(); } - if (this.settings.showDownloadRibbonButton) { - this.showDownloadAllRibbonIcon(); - } - - if (this.settings.showUploadModifiedFilesRibbonButton) { - this.showUploadModifiedFilesRibbonIcon(); - } - - if (this.settings.showUploadActiveFileRibbonButton) { - this.showUploadActiveFileRibbonIcon(); - } - - if (this.settings.showUploadAllFilesRibbonButton) { - this.showUploadAllFilesRibbonIcon(); + if (this.settings.showSyncRibbonButton) { + this.showSyncRibbonIcon(); } }); this.addCommand({ - id: "download-all-files", - name: "Download all files to GitHub", - repeatable: false, - icon: "arrow-down-from-line", - callback: async () => { - await this.syncManager.downloadAllFiles(); - this.updateStatusBarItem(); - }, - }); - - this.addCommand({ - id: "upload-modified-files", - name: "Upload modified files to GitHub", + id: "sync-files", + name: "Sync with GitHub", repeatable: false, icon: "refresh-cw", callback: async () => { - await this.syncManager.uploadModifiedFiles(); + await this.syncManager.sync(); this.updateStatusBarItem(); }, }); - - this.addCommand({ - id: "upload-active-file", - name: "Upload active file to GitHub", - repeatable: false, - icon: "arrow-up", - callback: async () => { - const activeView = this.app.workspace.getActiveViewOfType(FileView); - if (!activeView) { - return; - } - const activeFile = activeView.file; - if (!activeFile) { - return; - } - // await this.syncManager.uploadFile(activeFile); - this.updateStatusBarItem(); - }, - }); - - this.addCommand({ - id: "upload-all-files", - name: "Upload all files to GitHub", - repeatable: false, - icon: "arrow-up-from-line", - callback: async () => await this.openUploadAllFilesDialog(), - }); } async onunload() { @@ -129,16 +75,6 @@ export default class GitHubSyncPlugin extends Plugin { console.log("GitHubSyncPlugin unloaded"); } - /** - * Opens dialog to upload all file in the tracked folder. - * This doesn't take into account the state of the files and uploads - * ALL files whether they've been modified or not. - */ - private async openUploadAllFilesDialog() { - new UploadDialog(this).open(); - this.updateStatusBarItem(); - } - showStatusBarItem() { if (this.statusBarItem) { return; @@ -190,34 +126,15 @@ export default class GitHubSyncPlugin extends Plugin { this.statusBarItem.setText(`GitHub: ${state}`); } - showDownloadAllRibbonIcon() { - if (this.downloadAllRibbonIcon) { - return; - } - this.downloadAllRibbonIcon = this.addRibbonIcon( - "arrow-down-from-line", - "Download all files from GitHub", - async () => { - await this.syncManager.downloadAllFiles(); - this.updateStatusBarItem(); - }, - ); - } - - hideDownloadAllRibbonIcon() { - this.downloadAllRibbonIcon?.remove(); - this.downloadAllRibbonIcon = null; - } - - showUploadModifiedFilesRibbonIcon() { + showSyncRibbonIcon() { if (this.uploadModifiedFilesRibbonIcon) { return; } this.uploadModifiedFilesRibbonIcon = this.addRibbonIcon( "refresh-cw", - "Upload modified files to GitHub", + "Sync with GitHub", async () => { - await this.syncManager.uploadModifiedFiles(); + await this.syncManager.sync(); this.updateStatusBarItem(); }, ); @@ -228,50 +145,6 @@ export default class GitHubSyncPlugin extends Plugin { this.uploadModifiedFilesRibbonIcon = null; } - showUploadActiveFileRibbonIcon() { - if (this.uploadCurrentFileRibbonIcon) { - return; - } - - this.uploadCurrentFileRibbonIcon = this.addRibbonIcon( - "arrow-up", - "Upload current file to GitHub", - async () => { - const activeView = this.app.workspace.getActiveViewOfType(FileView); - if (!activeView) { - return; - } - const activeFile = activeView.file; - if (!activeFile) { - return; - } - // await this.syncManager.uploadFile(activeFile); - this.updateStatusBarItem(); - }, - ); - } - - hideUploadActiveFileRibbonIcon() { - this.uploadCurrentFileRibbonIcon?.remove(); - this.uploadCurrentFileRibbonIcon = null; - } - - showUploadAllFilesRibbonIcon() { - if (this.uploadAllFilesRibbonIcon) { - return; - } - this.uploadAllFilesRibbonIcon = this.addRibbonIcon( - "arrow-up-from-line", - "Upload all files to GitHub", - async () => await this.openUploadAllFilesDialog(), - ); - } - - hideUploadAllFilesRibbonIcon() { - this.uploadAllFilesRibbonIcon?.remove(); - this.uploadAllFilesRibbonIcon = null; - } - async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 1ad6da8..1bd8c74 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -10,10 +10,7 @@ export interface GitHubSyncSettings { syncOnStartup: boolean; conflictHandling: "ignore" | "ask" | "overwrite"; showStatusBarItem: boolean; - showDownloadRibbonButton: boolean; - showUploadModifiedFilesRibbonButton: boolean; - showUploadActiveFileRibbonButton: boolean; - showUploadAllFilesRibbonButton: boolean; + showSyncRibbonButton: boolean; } export const DEFAULT_SETTINGS: GitHubSyncSettings = { @@ -28,8 +25,5 @@ export const DEFAULT_SETTINGS: GitHubSyncSettings = { syncOnStartup: false, conflictHandling: "ask", showStatusBarItem: true, - showDownloadRibbonButton: true, - showUploadModifiedFilesRibbonButton: true, - showUploadActiveFileRibbonButton: true, - showUploadAllFilesRibbonButton: true, + showSyncRibbonButton: true, }; diff --git a/src/settings/tab.ts b/src/settings/tab.ts index d7e30c9..4fe7147 100644 --- a/src/settings/tab.ts +++ b/src/settings/tab.ts @@ -218,73 +218,20 @@ export default class GitHubSyncSettingsTab extends PluginSettingTab { }); new Setting(containerEl) - .setName("Show download all files button") - .setDesc( - "Displays a ribbon button to download all files from the remote repository", - ) + .setName("Show sync button") + .setDesc("Displays a ribbon button to sync files") .addToggle((toggle) => { toggle - .setValue(this.plugin.settings.showDownloadRibbonButton) + .setValue(this.plugin.settings.showSyncRibbonButton) .onChange((value) => { - this.plugin.settings.showDownloadRibbonButton = value; + this.plugin.settings.showSyncRibbonButton = value; this.plugin.saveSettings(); if (value) { - this.plugin.showDownloadAllRibbonIcon(); - } else { - this.plugin.hideDownloadAllRibbonIcon(); - } - }); - }); - - new Setting(containerEl) - .setName("Show upload modified files button") - .setDesc("Displays a ribbon button to upload modified files") - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.showUploadModifiedFilesRibbonButton) - .onChange((value) => { - this.plugin.settings.showUploadModifiedFilesRibbonButton = value; - this.plugin.saveSettings(); - if (value) { - this.plugin.showUploadModifiedFilesRibbonIcon(); + this.plugin.showSyncRibbonIcon(); } else { this.plugin.hideUploadModifiedFilesRibbonIcon(); } }); }); - - new Setting(containerEl) - .setName("Show upload active file button") - .setDesc("Displays a ribbon button to upload active file") - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.showUploadActiveFileRibbonButton) - .onChange((value) => { - this.plugin.settings.showUploadActiveFileRibbonButton = value; - this.plugin.saveSettings(); - if (value) { - this.plugin.showUploadActiveFileRibbonIcon(); - } else { - this.plugin.hideUploadActiveFileRibbonIcon(); - } - }); - }); - - new Setting(containerEl) - .setName("Show upload all files button") - .setDesc("Displays a ribbon button to upload all files") - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.showUploadAllFilesRibbonButton) - .onChange((value) => { - this.plugin.settings.showUploadAllFilesRibbonButton = value; - this.plugin.saveSettings(); - if (value) { - this.plugin.showUploadAllFilesRibbonIcon(); - } else { - this.plugin.hideUploadAllFilesRibbonIcon(); - } - }); - }); } } diff --git a/src/sync-manager.ts b/src/sync-manager.ts index 53390be..12404d4 100644 --- a/src/sync-manager.ts +++ b/src/sync-manager.ts @@ -5,7 +5,6 @@ import GithubClient, { } from "./github/client"; import MetadataStore, { FileMetadata, Metadata } from "./metadata-store"; import EventsListener from "./events/listener"; -import EventsConsumer from "./events/consumer"; import { GitHubSyncSettings } from "./settings/settings"; interface SyncAction { @@ -17,7 +16,6 @@ export default class SyncManager { private metadataStore: MetadataStore; private client: GithubClient; private eventsListener: EventsListener; - private eventsConsumer: EventsConsumer; private syncIntervalId: number | null = null; constructor( @@ -39,7 +37,6 @@ export default class SyncManager { this.settings.localContentDir, this.settings.repoContentDir, ); - this.eventsConsumer = new EventsConsumer(this); } async sync() { @@ -296,6 +293,7 @@ export default class SyncManager { // but not the same happens when the file is modified locally. // So if sha are different and the local file is newer we can't // know for sure which version should be kept. + return; } if (remoteFile.deleted && !localFile.deleted) { @@ -321,6 +319,10 @@ export default class SyncManager { } if (remoteFile.lastModified > localFile.lastModified) { + console.log("Downloading"); + console.log(filePath); + console.log(`localFile.lastModified: ${localFile.lastModified}`); + console.log(`remoteFile.lastModified: ${remoteFile.lastModified}`); actions.push({ type: "download", filePath: filePath }); } else if (localFile.lastModified > remoteFile.lastModified) { console.log("Uploading"); @@ -402,22 +404,6 @@ export default class SyncManager { await this.client.updateBranchHead(commitSha); } - async uploadFile(filePath: string) { - const { remotePath, sha } = this.metadataStore.data.files[filePath]; - const normalizedFilePath = normalizePath(filePath); - if (!(await this.vault.adapter.exists(normalizedFilePath))) { - throw new Error(`Can't find file ${filePath}`); - } - const fileContent = await this.vault.adapter.readBinary(normalizedFilePath); - await this.client.uploadFile(remotePath, fileContent, sha); - // Reset dirty state - this.metadataStore.data.files[filePath].dirty = false; - // Gets the new SHA of the file - const newSha = await this.client.getFileSha(remotePath); - this.metadataStore.data.files[filePath].sha = newSha; - this.metadataStore.save(); - } - async downloadFile(file: GetTreeResponseItem, lastModified: number) { const url = file.url; const destinationFile = file.path.replace( @@ -460,52 +446,6 @@ export default class SyncManager { this.metadataStore.save(); } - async deleteRemoteFile(filePath: string) { - const { remotePath, sha } = this.metadataStore.data.files[filePath]; - if (!sha) { - // File was never uploaded, no need to delete it - // This is unlikely to happen but we handle it anyway - return; - } - await this.client.deleteFile(remotePath, sha); - this.metadataStore.save(); - } - - async deleteFile(filePath: string) { - const { remotePath, sha } = this.metadataStore.data.files[filePath]; - if (!sha) { - // File was never uploaded, no need to delete it - return; - } - await this.client.deleteFile(remotePath, sha); - this.metadataStore.save(); - } - - async downloadAllFiles() { - const { files } = await this.client.getRepoContent(); - - await Promise.all( - Object.keys(files) - .filter((filePath: string) => - filePath.startsWith(this.settings.repoContentDir), - ) - .map( - async (filePath: string) => - await this.downloadFile(files[filePath], Date.now()), - ), - ); - } - - async uploadModifiedFiles() { - // We upload files sequentially to avoid conflicts. - // GitHub rejects commits if they're made in fast succession, thus - // forcing us to retry the failed upload. - // So parallelization is not an option. - for (const event of this.eventsListener.flush()) { - await this.eventsConsumer.process(event); - } - } - async loadMetadata() { await this.metadataStore.load(); if (Object.keys(this.metadataStore.data.files).length === 0) { @@ -577,8 +517,7 @@ export default class SyncManager { this.syncIntervalId = window.setInterval( () => this.sync(), // Sync interval is set in minutes but setInterval expects milliseconds - // minutes * 60 * 1000, - 10000, + minutes * 60 * 1000, ); return this.syncIntervalId; } diff --git a/src/views/upload-all-files-dialog/component.tsx b/src/views/upload-all-files-dialog/component.tsx deleted file mode 100644 index e98e4a0..0000000 --- a/src/views/upload-all-files-dialog/component.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { usePlugin } from "../hooks"; -import { useState, useMemo, useEffect } from "react"; - -const UploadDialogQuestion = ({ - fileCount, - onCancel, - startUpload: onUpload, -}: { - fileCount: number; - onCancel: () => void; - startUpload: () => Promise; -}) => { - return ( -
-

Upload files to GitHub

-

- Do you want to upload{" "} - {fileCount} files? This - might take a while. -

-
- - -
-
- ); -}; - -const UploadDialogUploading = ({ - completedCount, - fileCount, - onCancel, -}: { - completedCount: number; - fileCount: number; - onCancel: () => void; -}) => { - const progress = (completedCount * 100) / fileCount; - - return ( -
-

- {progress === 100 ? "All files uploaded" : "Uploading…"} -

-
-
-
-

- Uploaded {completedCount} file{completedCount === 1 ? "" : "s"} of{" "} - {fileCount} -

-
- {/* - We use onCancel for both cases as it won't have any effect other - than closing the modal if upload is done - */} - -
-
- ); -}; - -const UploadDialogContent = ({ onCancel }: { onCancel: () => void }) => { - const plugin = usePlugin(); - if (!plugin) { - // Unlikely to happen, makes TS happy though - throw new Error("Plugin is not initialized"); - } - const [uploading, setUploading] = useState(false); - const [completedCount, setCompletedCount] = useState(0); - - const files = useMemo( - // This is not the most efficient way to get all files in the - // folder to sync but it's quick to code. - // I'll enhance it in the future. - () => - plugin?.app.vault - .getFiles() - .filter((file) => - file.path.startsWith(plugin.settings.localContentDir), - ), - [], - ); - - const abortController = useMemo(() => new AbortController(), []); - - // Stop the upload when the component is unmounted, otherwise - // we keep uploading even when the modal is closed. - useEffect(() => { - return () => { - abortController.abort(); - }; - }, []); - - const startUploading = async () => { - setUploading(true); - // We upload files sequentially to avoid conflicts. - // GitHub rejects commits if they're made in fast succession, thus - // forcing us to retry the failed upload. - // So parallelization is not an option. - for (const file of files) { - if (abortController.signal.aborted) { - return; - } - await plugin.syncManager.uploadFile(file); - if (abortController.signal.aborted) { - // Check abort state only after the uploaded file metadata - // is updated, otherwise we risk having outdated SHAs - return; - } - setCompletedCount((count) => count + 1); - } - }; - - return ( -
- {!uploading && ( - - )} - {uploading && ( - { - abortController.abort(); - onCancel(); - }} - /> - )} -
- ); -}; - -export default UploadDialogContent; diff --git a/src/views/upload-all-files-dialog/view.tsx b/src/views/upload-all-files-dialog/view.tsx deleted file mode 100644 index d563f2e..0000000 --- a/src/views/upload-all-files-dialog/view.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Modal } from "obsidian"; -import { Root, createRoot } from "react-dom/client"; -import GitHubSyncPlugin from "src/main"; -import { PluginContext } from "../hooks"; -import UploadDialogContent from "./component"; - -/** - * Dialog shown when user tries to upload all local files to GitHub. - */ -export class UploadDialog extends Modal { - root: Root | null = null; - - constructor(private plugin: GitHubSyncPlugin) { - super(plugin.app); - } - - onOpen() { - this.root = createRoot(this.modalEl); - this.root.render( - - this.close()} /> - , - ); - } - - onClose() { - const { contentEl } = this; - // It's important to unmount the component so upload stops - // in case it's running - this.root?.unmount(); - contentEl.empty(); - } -}