From 4085c2f5c5bd461250106998ca104b5156631363 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Tue, 14 Jan 2025 16:21:49 +0100 Subject: [PATCH] Ugly, uncommented and messy but kinda works --- src/events/listener.ts | 57 +++-- src/github/client.ts | 81 ++++++- src/main.ts | 4 +- src/metadata-store.ts | 11 +- src/sync-manager.ts | 482 +++++++++++++++++++++++++++++++++++++++-- 5 files changed, 579 insertions(+), 56 deletions(-) diff --git a/src/events/listener.ts b/src/events/listener.ts index 24ecb3f..02ef940 100644 --- a/src/events/listener.ts +++ b/src/events/listener.ts @@ -31,7 +31,7 @@ export default class EventsListener { } private async onCreate(file: TAbstractFile) { - if (!file.path.startsWith(this.localContentDir)) { + if (!this.isSyncable(file.path)) { // The file has not been created in directory that we're syncing with GitHub return; } @@ -40,22 +40,34 @@ export default class EventsListener { return; } - const data = this.metadataStore.data[file.path]; + const data = this.metadataStore.data.files[file.path]; if (data && data.justDownloaded) { // This file was just downloaded and not created by the user. // It's enough to mark it as non just downloaded. - this.metadataStore.data[file.path].justDownloaded = false; + this.metadataStore.data.files[file.path].justDownloaded = false; await this.metadataStore.save(); return; } - this.metadataStore.data[file.path] = { + let remotePath: string; + if (file.path.startsWith(this.localContentDir)) { + remotePath = file.path.replace(this.localContentDir, this.repoContentDir); + } else if ( + file.path.startsWith(`${this.vault.configDir}/github-sync-metadata.json`) + ) { + remotePath = file.path; + } else { + throw new Error("Unexpected file path"); + } + + this.metadataStore.data.files[file.path] = { localPath: file.path, - remotePath: file.path.replace(this.localContentDir, this.repoContentDir), + remotePath: remotePath!, sha: null, dirty: true, // This file has been created by the user justDownloaded: false, + lastModified: Date.now(), }; await this.metadataStore.save(); this.eventsQueue.enqueue({ @@ -70,11 +82,14 @@ export default class EventsListener { return; } const filePath = file instanceof TAbstractFile ? file.path : file; - if (!filePath.startsWith(this.localContentDir)) { + if (!this.isSyncable(filePath)) { // The file was not in directory that we're syncing with GitHub return; } + this.metadataStore.data.files[filePath].deleted = true; + this.metadataStore.data.files[filePath].deletedAt = Date.now(); + await this.metadataStore.save(); // We don't delete metadata as we need that info when calling the API // to delete the file. // We'll delete them later. @@ -85,7 +100,7 @@ export default class EventsListener { } private async onModify(file: TAbstractFile) { - if (!file.path.startsWith(this.localContentDir)) { + if (!this.isSyncable(file.path)) { // The file has not been create in directory that we're syncing with GitHub return; } @@ -93,15 +108,16 @@ export default class EventsListener { // Skip folders return; } - const data = this.metadataStore.data[file.path]; + const data = this.metadataStore.data.files[file.path]; if (data && data.justDownloaded) { // This file was just downloaded and not modified by the user. // It's enough to makr it as non just downloaded. - this.metadataStore.data[file.path].justDownloaded = false; + this.metadataStore.data.files[file.path].justDownloaded = false; await this.metadataStore.save(); return; } - this.metadataStore.data[file.path].dirty = true; + this.metadataStore.data.files[file.path].lastModified = Date.now(); + this.metadataStore.data.files[file.path].dirty = true; await this.metadataStore.save(); this.eventsQueue.enqueue({ type: "modify", @@ -114,32 +130,33 @@ export default class EventsListener { // Skip folders return; } - if ( - !file.path.startsWith(this.localContentDir) && - !oldPath.startsWith(this.localContentDir) - ) { + if (!this.isSyncable(file.path) && !this.isSyncable(oldPath)) { // Both are not in directory that we're syncing with GitHub return; } - if ( - file.path.startsWith(this.localContentDir) && - oldPath.startsWith(this.localContentDir) - ) { + if (this.isSyncable(file.path) && this.isSyncable(oldPath)) { // Both files are in the synced directory // First create the new one await this.onCreate(file); // Then delete the old one await this.onDelete(oldPath); return; - } else if (file.path.startsWith(this.localContentDir)) { + } else if (this.isSyncable(file.path)) { // Only the new file is in the local directory await this.onCreate(file); return; - } else if (oldPath.startsWith(this.localContentDir)) { + } else if (this.isSyncable(oldPath)) { // Only the old file was in the local directory await this.onDelete(oldPath); return; } } + + private isSyncable(filePath: string) { + return ( + filePath.startsWith(this.localContentDir) || + filePath.startsWith(`${this.vault.configDir}/github-sync-metadata.json`) + ); + } } diff --git a/src/github/client.ts b/src/github/client.ts index b53db94..b126e5a 100644 --- a/src/github/client.ts +++ b/src/github/client.ts @@ -1,10 +1,15 @@ import { Vault, requestUrl, normalizePath } from "obsidian"; -import MetadataStore from "../metadata-store"; +import MetadataStore, { Metadata } from "../metadata-store"; + +export type RepoContent = { + files: { [key: string]: GetTreeResponseItem }; + sha: string; +}; /** * Represents a single item in a tree response from the GitHub API. */ -export type TreeItem = { +export type GetTreeResponseItem = { path: string; mode: string; type: string; @@ -13,6 +18,14 @@ export type TreeItem = { url: string; }; +export type NewTreeRequestItem = { + path: string; + mode: string; + type: string; + sha?: string | null; + content?: string; +}; + /** * Represents a git blob response from the GitHub API. */ @@ -32,6 +45,7 @@ export default class GithubClient { private repo: string, private branch: string, private repoContentDir: string, + private configDir: string, ) {} headers() { @@ -43,21 +57,68 @@ export default class GithubClient { } /** - * Gets the content of a directory in the repo. - * Or the whole repo if repoContentDir is an empty string. + * Gets the content of the repo. * * @returns Array of files in the directory in the remote repo */ - async getRepoContent(): Promise { + async getRepoContent(): Promise { const res = await requestUrl({ url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/trees/${this.branch}?recursive=1`, headers: this.headers(), }); - const files = res.json["tree"].filter( - (file: TreeItem) => - file.type === "blob" && file.path.startsWith(this.repoContentDir), - ); - return files; + const files = res.json.tree + .filter((file: GetTreeResponseItem) => file.type === "blob") + .reduce( + ( + acc: { [key: string]: GetTreeResponseItem }, + file: GetTreeResponseItem, + ) => ({ ...acc, [file.path]: file }), + {}, + ); + return { files, sha: res.json.sha }; + } + + async createTree(tree: { tree: NewTreeRequestItem[]; base_tree: string }) { + const res = await requestUrl({ + url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/trees`, + headers: this.headers(), + method: "POST", + body: JSON.stringify(tree), + }); + return res.json.sha; + } + + async createCommit(message: string, treeSha: string, parent: string) { + const res = await requestUrl({ + url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/commits`, + headers: this.headers(), + method: "POST", + body: JSON.stringify({ + message: message, + tree: treeSha, + parents: [parent], + }), + }); + return res.json.sha; + } + + async getBranchHeadSha() { + const res = await requestUrl({ + url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`, + headers: this.headers(), + }); + return res.json.object.sha; + } + + async updateBranchHead(sha: string) { + await requestUrl({ + url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`, + headers: this.headers(), + method: "PATCH", + body: JSON.stringify({ + sha: sha, + }), + }); } /** diff --git a/src/main.ts b/src/main.ts index 52c0091..812b25e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -109,7 +109,7 @@ export default class GitHubSyncPlugin extends Plugin { if (!activeFile) { return; } - await this.syncManager.uploadFile(activeFile); + // await this.syncManager.uploadFile(activeFile); this.updateStatusBarItem(); }, }); @@ -245,7 +245,7 @@ export default class GitHubSyncPlugin extends Plugin { if (!activeFile) { return; } - await this.syncManager.uploadFile(activeFile); + // await this.syncManager.uploadFile(activeFile); this.updateStatusBarItem(); }, ); diff --git a/src/metadata-store.ts b/src/metadata-store.ts index 8980efb..aca23a9 100644 --- a/src/metadata-store.ts +++ b/src/metadata-store.ts @@ -20,10 +20,17 @@ export interface FileMetadata { // downloaded it will trigger a 'create' or 'modify' event. // This is a problem as we can't know whether an event has been triggered by us or the user. justDownloaded: boolean; + // The last time the file was modified + lastModified: number; + // Whether the file has been deleted + deleted?: boolean | null; + // When the file was deleted + deletedAt?: number | null; } export interface Metadata { - [key: string]: FileMetadata; + lastSync: number; + files: { [key: string]: FileMetadata }; } /** @@ -50,7 +57,7 @@ export default class MetadataStore { const content = await this.vault.adapter.read(this.metadataFile); this.data = JSON.parse(content); } else { - this.data = {}; + this.data = { lastSync: 0, files: {} }; } } diff --git a/src/sync-manager.ts b/src/sync-manager.ts index 4c98ded..53390be 100644 --- a/src/sync-manager.ts +++ b/src/sync-manager.ts @@ -1,10 +1,18 @@ -import { Vault, TFile, normalizePath } from "obsidian"; -import GithubClient, { TreeItem } from "./github/client"; -import MetadataStore, { FileMetadata } from "./metadata-store"; +import { Vault, normalizePath } from "obsidian"; +import GithubClient, { + GetTreeResponseItem, + NewTreeRequestItem, +} 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 { + type: "upload" | "download" | "delete_local" | "delete_remote"; + filePath: string; +} + export default class SyncManager { private metadataStore: MetadataStore; private client: GithubClient; @@ -23,6 +31,7 @@ export default class SyncManager { this.settings.githubRepo, this.settings.githubBranch, this.settings.repoContentDir, + this.vault.configDir, ); this.eventsListener = new EventsListener( this.vault, @@ -34,33 +43,388 @@ export default class SyncManager { } async sync() { - // TODO + // TODO: Handle cases + // 1. Remote manifest not found + // 2. Local manifest not found + // 3. Remote repo and local vault folder have both some files + // + // These could very well be first sync, maybe a separate function might be best. + // Might be something for onboarding. + const { files, sha: treeSha } = await this.client.getRepoContent(); + const manifest = files[`${this.vault.configDir}/github-sync-metadata.json`]; + + if (manifest === undefined && Object.keys(files).length > 0) { + const localContentDirExists = await this.vault.adapter.exists( + this.settings.localContentDir, + ); + if (localContentDirExists) { + const existing = await this.vault.adapter.list( + this.settings.localContentDir, + ); + if ( + (this.settings.localContentDir === "" && + existing.folders.length > 1) || + existing.files.length > 0 + ) { + // There are local and remote files but remote doesn't have a manifest + // we don't know what to do in this case + throw new Error("Can't sync"); + } else if (existing.folders.length > 0 || existing.files.length > 0) { + // There are local and remote files but remote doesn't have a manifest + // we don't know what to do in this case + throw new Error("Can't sync"); + } + } + + // There's no remote manifest and there are files in the repo, + // though there are no files locally either, so we can just download everything + // and sync the remote manifest. + await Promise.all( + Object.keys(files) + .filter((filePath: string) => { + if (filePath.startsWith(this.settings.repoContentDir)) { + return true; + } else if (filePath.startsWith(this.vault.configDir)) { + return true; + } + return false; + }) + .map(async (filePath: string) => { + await this.downloadFile(files[filePath], Date.now()); + }), + ); + const newTreeFiles = Object.keys(files) + .map((filePath: string) => ({ + path: files[filePath].path, + mode: files[filePath].mode, + type: files[filePath].type, + sha: files[filePath].sha, + })) + .reduce( + ( + acc: { [key: string]: NewTreeRequestItem }, + item: NewTreeRequestItem, + ) => ({ ...acc, [item.path]: item }), + {}, + ); + // Add files that are in the manifest but not in the tree. + await Promise.all( + Object.keys(this.metadataStore.data.files).map( + async (filePath: string) => { + const normalizedPath = normalizePath(filePath); + const content = await this.vault.adapter.read(normalizedPath); + const { remotePath } = this.metadataStore.data.files[filePath]; + newTreeFiles[remotePath] = { + path: remotePath, + mode: "100644", + type: "blob", + content: content, + }; + }, + ), + ); + await this.commitSync(newTreeFiles, treeSha); + return; + } + + const blob = await this.client.getBlob(manifest.url); + const remoteMetadata: Metadata = JSON.parse(atob(blob.content)); + + const conflicts = this.findConflicts( + remoteMetadata.files, + this.metadataStore.data.files, + ); + if (conflicts.length > 0) { + // TODO: Show conflicts to the user with a callback + // Wait for response + // Create new action to handle the conflict + // Add the new action to the list later on + console.log("Conflicts"); + console.log(conflicts); + } + + const actions = this.determineSyncActions( + remoteMetadata.files, + this.metadataStore.data.files, + ); + + if (actions.length === 0) { + console.log("Nothing to sync"); + return; + } else { + console.log("Actions:"); + console.log(actions); + } + + const newTreeFiles: { [key: string]: NewTreeRequestItem } = Object.keys( + files, + ) + .map((filePath: string) => ({ + path: files[filePath].path, + mode: files[filePath].mode, + type: files[filePath].type, + sha: files[filePath].sha, + })) + .reduce( + ( + acc: { [key: string]: NewTreeRequestItem }, + item: NewTreeRequestItem, + ) => ({ ...acc, [item.path]: item }), + {}, + ); + + await Promise.all( + actions.map(async (action) => { + switch (action.type) { + case "upload": { + const normalizedPath = normalizePath(action.filePath); + const content = await this.vault.adapter.read(normalizedPath); + const remotePath = action.filePath.replace( + this.settings.localContentDir, + this.settings.repoContentDir, + ); + newTreeFiles[remotePath] = { + path: remotePath, + mode: "100644", + type: "blob", + content: content, + }; + break; + } + case "delete_remote": { + const { remotePath } = + this.metadataStore.data.files[action.filePath]; + newTreeFiles[remotePath].sha = null; + break; + } + case "download": + break; + case "delete_local": + break; + } + }), + ); + + // Download files and delete local files + await Promise.all([ + ...actions + .filter((action) => action.type === "download") + .map(async (action: SyncAction) => { + const remotePath = action.filePath.replace( + this.settings.localContentDir, + this.settings.repoContentDir, + ); + await this.downloadFile( + files[remotePath], + remoteMetadata.files[action.filePath].lastModified, + ); + }), + ...actions + .filter((action) => action.type === "delete_local") + .map(async (action: SyncAction) => { + await this.deleteLocalFile(action.filePath); + }), + ]); + + await this.commitSync(newTreeFiles, treeSha); } - async uploadFile(file: TFile) { - // TODO: Remove the file from eventsListener if it's there - const { remotePath, sha } = this.metadataStore.data[file.path]; - const normalizedFilePath = normalizePath(file.path); + findConflicts( + remoteFiles: { [key: string]: FileMetadata }, + localFiles: { [key: string]: FileMetadata }, + ): { remoteFile: FileMetadata; localFile: FileMetadata }[] { + const commonFiles = Object.keys(remoteFiles).filter( + (key) => key in localFiles, + ); + + return commonFiles + .map((filePath: string) => { + const remoteFile = remoteFiles[filePath]; + const localFile = localFiles[filePath]; + + // We compare the SHA cause it remote files changes the SHA changes + // 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. + if ( + remoteFile.sha !== localFile.sha && + remoteFile.lastModified < localFile.lastModified + ) { + // File is modified on both sides, the user must solve the conflict + return { + remoteFile: remoteFile, + localFile: localFile, + }; + } + return null; + }) + .filter( + ( + conflict: { + remoteFile: FileMetadata; + localFile: FileMetadata; + } | null, + ) => conflict !== null, + ); + } + + determineSyncActions( + remoteFiles: { [key: string]: FileMetadata }, + localFiles: { [key: string]: FileMetadata }, + ) { + let actions: SyncAction[] = []; + + const commonFiles = Object.keys(remoteFiles).filter( + (key) => key in localFiles, + ); + + // Get diff for common files + commonFiles.forEach((filePath: string) => { + const remoteFile = remoteFiles[filePath]; + const localFile = localFiles[filePath]; + if (remoteFile.deleted && localFile.deleted) { + // Nothing to do + return; + } + + if ( + remoteFile.sha !== localFile.sha && + remoteFile.lastModified < localFile.lastModified + ) { + // This is a conflict, we handle it separately + // We compare the SHA cause it remote files changes the SHA changes + // 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. + } + + if (remoteFile.deleted && !localFile.deleted) { + if ((remoteFile.deletedAt as number) > localFile.lastModified) { + actions.push({ + type: "delete_local", + filePath: filePath, + }); + } else if (localFile.lastModified > (remoteFile.deletedAt as number)) { + actions.push({ type: "upload", filePath: filePath }); + } + } + + if (!remoteFile.deleted && localFile.deleted) { + if (remoteFile.lastModified > (localFile.deletedAt as number)) { + actions.push({ type: "download", filePath: filePath }); + } else if ((localFile.deletedAt as number) > remoteFile.lastModified) { + actions.push({ + type: "delete_remote", + filePath: filePath, + }); + } + } + + if (remoteFile.lastModified > localFile.lastModified) { + actions.push({ type: "download", filePath: filePath }); + } else if (localFile.lastModified > remoteFile.lastModified) { + console.log("Uploading"); + console.log(filePath); + console.log(`localFile.lastModified: ${localFile.lastModified}`); + console.log(`remoteFile.lastModified: ${remoteFile.lastModified}`); + actions.push({ type: "upload", filePath: filePath }); + } + }); + + // Get diff for files in remote but not in local + Object.keys(remoteFiles).forEach((filePath: string) => { + const remoteFile = remoteFiles[filePath]; + const localFile = localFiles[filePath]; + if (localFile) { + // Local file exists, we already handled it. + // Skip it. + return; + } + if (remoteFile.deleted) { + // Remote is deleted but we don't have it locally. + // Nothing to do. + // TODO: Maybe we need to remove remote reference too? + } else { + actions.push({ type: "download", filePath: filePath }); + } + }); + + // Get diff for files in local but not in remote + Object.keys(localFiles).forEach((filePath: string) => { + const remoteFile = remoteFiles[filePath]; + const localFile = localFiles[filePath]; + if (remoteFile) { + // Remote file exists, we already handled it. + // Skip it. + return; + } + if (localFile.deleted) { + // Local is deleted and remote doesn't exist. + // Just remove the local reference. + } else { + actions.push({ type: "upload", filePath: filePath }); + } + }); + + return actions; + } + + async commitSync( + treeFiles: { [key: string]: NewTreeRequestItem }, + baseTreeSha: string, + ) { + // Update local sync time + this.metadataStore.data.lastSync = Date.now(); + this.metadataStore.save(); + + // Update manifest in list of new tree items + delete treeFiles[`${this.vault.configDir}/github-sync-metadata.json`].sha; + treeFiles[`${this.vault.configDir}/github-sync-metadata.json`].content = + JSON.stringify(this.metadataStore.data); + + // Create the new tree + const newTree: { tree: NewTreeRequestItem[]; base_tree: string } = { + tree: Object.keys(treeFiles).map( + (filePath: string) => treeFiles[filePath], + ), + base_tree: baseTreeSha, + }; + const newTreeSha = await this.client.createTree(newTree); + + const branchHeadSha = await this.client.getBranchHeadSha(); + + const commitSha = await this.client.createCommit( + "Sync", + newTreeSha, + branchHeadSha, + ); + + 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 ${file.path}`); + 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[file.path].dirty = false; + this.metadataStore.data.files[filePath].dirty = false; // Gets the new SHA of the file const newSha = await this.client.getFileSha(remotePath); - this.metadataStore.data[file.path].sha = newSha; + this.metadataStore.data.files[filePath].sha = newSha; this.metadataStore.save(); } - async downloadFile(file: TreeItem) { + async downloadFile(file: GetTreeResponseItem, lastModified: number) { const url = file.url; const destinationFile = file.path.replace( this.settings.repoContentDir, this.settings.localContentDir, ); - const fileMetadata = this.metadataStore.data[destinationFile]; + const fileMetadata = this.metadataStore.data.files[destinationFile]; if (fileMetadata && fileMetadata.sha === file.sha) { // File already exists and has the same SHA, no need to download it again. return; @@ -77,33 +441,58 @@ export default class SyncManager { normalizePath(destinationFile), Buffer.from(blob.content, "base64"), ); - this.metadataStore.data[destinationFile] = { + this.metadataStore.data.files[destinationFile] = { localPath: destinationFile, remotePath: file.path, sha: file.sha, dirty: false, justDownloaded: true, + lastModified: lastModified, }; await this.metadataStore.save(); } + async deleteLocalFile(filePath: string) { + const normalizedPath = normalizePath(filePath); + await this.vault.adapter.remove(normalizedPath); + this.metadataStore.data.files[filePath].deleted = true; + this.metadataStore.data.files[filePath].deletedAt = Date.now(); + 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[filePath]; + 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); - // File has been deleted, no need to keep track of it anymore - delete this.metadataStore.data[filePath]; this.metadataStore.save(); } async downloadAllFiles() { - const files = await this.client.getRepoContent(); + const { files } = await this.client.getRepoContent(); await Promise.all( - files.map(async (file: TreeItem) => await this.downloadFile(file)), + Object.keys(files) + .filter((filePath: string) => + filePath.startsWith(this.settings.repoContentDir), + ) + .map( + async (filePath: string) => + await this.downloadFile(files[filePath], Date.now()), + ), ); } @@ -119,10 +508,58 @@ export default class SyncManager { async loadMetadata() { await this.metadataStore.load(); + if (Object.keys(this.metadataStore.data.files).length === 0) { + // Must be the first time we run, initialize the metadata store + // with the files from the config directory + let files = []; + let folders = [this.vault.configDir]; + while (folders.length > 0) { + const folder = folders.pop(); + if (!folder) { + continue; + } + const res = await this.vault.adapter.list(folder); + files.push(...res.files); + folders.push(...res.folders); + } + files.forEach((filePath: string) => { + if (filePath === `${this.vault.configDir}/workspace.json`) { + // Obsidian recommends not syncing the workspace file + return; + } + if (filePath.startsWith(`${this.vault.configDir}/plugins`)) { + // Let's not sync plugins for the time being + return; + } + this.metadataStore.data.files[filePath] = { + localPath: filePath, + // The config dir is always stored in the repo root so we use + // the same path for remote + remotePath: filePath, + sha: null, + dirty: false, + justDownloaded: false, + lastModified: Date.now(), + }; + }); + // Add itself, if we're here the manifest file doesn't exist + // so it can't add itself to the list of files + this.metadataStore.data.files[ + `${this.vault.configDir}/github-sync-metadata.json` + ] = { + localPath: `${this.vault.configDir}/github-sync-metadata.json`, + remotePath: `${this.vault.configDir}/github-sync-metadata.json`, + sha: null, + dirty: false, + justDownloaded: false, + lastModified: Date.now(), + }; + this.metadataStore.save(); + } } getFileMetadata(filePath: string): FileMetadata { - return this.metadataStore.data[filePath]; + return this.metadataStore.data.files[filePath]; } async startEventsListener() { @@ -138,9 +575,10 @@ export default class SyncManager { throw new Error("Sync interval is already running"); } this.syncIntervalId = window.setInterval( - () => this.uploadModifiedFiles(), + () => this.sync(), // Sync interval is set in minutes but setInterval expects milliseconds - minutes * 60 * 1000, + // minutes * 60 * 1000, + 10000, ); return this.syncIntervalId; }