Add logger

This commit is contained in:
Silvano Cerza 2025-01-23 15:10:15 +01:00
parent 0ca0be97f6
commit 2c4ed3e671
6 changed files with 137 additions and 3 deletions

View file

@ -1,6 +1,7 @@
import { Vault, TAbstractFile, TFolder } from "obsidian"; import { Vault, TAbstractFile, TFolder } from "obsidian";
import MetadataStore from "./metadata-store"; import MetadataStore from "./metadata-store";
import { GitHubSyncSettings } from "./settings/settings"; import { GitHubSyncSettings } from "./settings/settings";
import Logger from "./logger";
/** /**
* Tracks changes to local sync directory and updates files metadata. * Tracks changes to local sync directory and updates files metadata.
@ -10,6 +11,7 @@ export default class EventsListener {
private vault: Vault, private vault: Vault,
private metadataStore: MetadataStore, private metadataStore: MetadataStore,
private settings: GitHubSyncSettings, private settings: GitHubSyncSettings,
private logger: Logger,
) {} ) {}
start() { start() {
@ -20,8 +22,10 @@ export default class EventsListener {
} }
private async onCreate(file: TAbstractFile) { private async onCreate(file: TAbstractFile) {
await this.logger.info("Received create event", file.path);
if (!this.isSyncable(file.path)) { if (!this.isSyncable(file.path)) {
// The file has not been created in directory that we're syncing with GitHub // The file has not been created in directory that we're syncing with GitHub
await this.logger.info("Skipped created file", file.path);
return; return;
} }
if (file instanceof TFolder) { if (file instanceof TFolder) {
@ -35,6 +39,7 @@ export default class EventsListener {
// It's enough to mark it as non just downloaded. // It's enough to mark it as non just downloaded.
this.metadataStore.data.files[file.path].justDownloaded = false; this.metadataStore.data.files[file.path].justDownloaded = false;
await this.metadataStore.save(); await this.metadataStore.save();
await this.logger.info("Updated just downloaded created file", file.path);
return; return;
} }
@ -47,14 +52,16 @@ export default class EventsListener {
lastModified: Date.now(), lastModified: Date.now(),
}; };
await this.metadataStore.save(); await this.metadataStore.save();
await this.logger.info("Updated created file", file.path);
} }
private async onDelete(file: TAbstractFile | string) { private async onDelete(file: TAbstractFile | string) {
const filePath = file instanceof TAbstractFile ? file.path : file;
await this.logger.info("Received delete event", filePath);
if (file instanceof TFolder) { if (file instanceof TFolder) {
// Skip folders // Skip folders
return; return;
} }
const filePath = file instanceof TAbstractFile ? file.path : file;
if (!this.isSyncable(filePath)) { if (!this.isSyncable(filePath)) {
// The file was not in directory that we're syncing with GitHub // The file was not in directory that we're syncing with GitHub
return; return;
@ -63,11 +70,14 @@ export default class EventsListener {
this.metadataStore.data.files[filePath].deleted = true; this.metadataStore.data.files[filePath].deleted = true;
this.metadataStore.data.files[filePath].deletedAt = Date.now(); this.metadataStore.data.files[filePath].deletedAt = Date.now();
await this.metadataStore.save(); await this.metadataStore.save();
await this.logger.info("Updated deleted file", filePath);
} }
private async onModify(file: TAbstractFile) { private async onModify(file: TAbstractFile) {
await this.logger.info("Received modify event", file.path);
if (!this.isSyncable(file.path)) { if (!this.isSyncable(file.path)) {
// The file has not been create in directory that we're syncing with GitHub // The file has not been create in directory that we're syncing with GitHub
await this.logger.info("Skipped modified file", file.path);
return; return;
} }
if (file instanceof TFolder) { if (file instanceof TFolder) {
@ -80,14 +90,20 @@ export default class EventsListener {
// It's enough to makr it as non just downloaded. // It's enough to makr it as non just downloaded.
this.metadataStore.data.files[file.path].justDownloaded = false; this.metadataStore.data.files[file.path].justDownloaded = false;
await this.metadataStore.save(); await this.metadataStore.save();
await this.logger.info(
"Updated just downloaded modified file",
file.path,
);
return; return;
} }
this.metadataStore.data.files[file.path].lastModified = Date.now(); this.metadataStore.data.files[file.path].lastModified = Date.now();
this.metadataStore.data.files[file.path].dirty = true; this.metadataStore.data.files[file.path].dirty = true;
await this.metadataStore.save(); await this.metadataStore.save();
await this.logger.info("Updated modified file", file.path);
} }
private async onRename(file: TAbstractFile, oldPath: string) { private async onRename(file: TAbstractFile, oldPath: string) {
await this.logger.info("Received rename event", file.path);
if (file instanceof TFolder) { if (file instanceof TFolder) {
// Skip folders // Skip folders
return; return;

View file

@ -1,4 +1,5 @@
import { requestUrl } from "obsidian"; import { requestUrl } from "obsidian";
import Logger from "src/logger";
export type RepoContent = { export type RepoContent = {
files: { [key: string]: GetTreeResponseItem }; files: { [key: string]: GetTreeResponseItem };
@ -43,6 +44,7 @@ export default class GithubClient {
private owner: string, private owner: string,
private repo: string, private repo: string,
private branch: string, private branch: string,
private logger: Logger,
) {} ) {}
headers() { headers() {
@ -62,7 +64,12 @@ export default class GithubClient {
const res = await requestUrl({ const res = await requestUrl({
url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/trees/${this.branch}?recursive=1`, url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/trees/${this.branch}?recursive=1`,
headers: this.headers(), headers: this.headers(),
throw: false,
}); });
if (res.status < 200 || res.status >= 400) {
await this.logger.error("Failed to get repo content", res);
throw new Error(`Failed to get repo content, status ${res.status}`);
}
const files = res.json.tree const files = res.json.tree
.filter((file: GetTreeResponseItem) => file.type === "blob") .filter((file: GetTreeResponseItem) => file.type === "blob")
.reduce( .reduce(
@ -81,7 +88,12 @@ export default class GithubClient {
headers: this.headers(), headers: this.headers(),
method: "POST", method: "POST",
body: JSON.stringify(tree), body: JSON.stringify(tree),
throw: false,
}); });
if (res.status < 200 || res.status >= 400) {
await this.logger.error("Failed to create tree", res);
throw new Error(`Failed to create tree, status ${res.status}`);
}
return res.json.sha; return res.json.sha;
} }
@ -95,7 +107,12 @@ export default class GithubClient {
tree: treeSha, tree: treeSha,
parents: [parent], parents: [parent],
}), }),
throw: false,
}); });
if (res.status < 200 || res.status >= 400) {
await this.logger.error("Failed to create commit", res);
throw new Error(`Failed to create commit, status ${res.status}`);
}
return res.json.sha; return res.json.sha;
} }
@ -103,19 +120,29 @@ export default class GithubClient {
const res = await requestUrl({ const res = await requestUrl({
url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`, url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`,
headers: this.headers(), headers: this.headers(),
throw: false,
}); });
if (res.status < 200 || res.status >= 400) {
await this.logger.error("Failed to get branch head sha", res);
throw new Error(`Failed to get branch head sha, status ${res.status}`);
}
return res.json.object.sha; return res.json.object.sha;
} }
async updateBranchHead(sha: string) { async updateBranchHead(sha: string) {
await requestUrl({ const res = await requestUrl({
url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`, url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`,
headers: this.headers(), headers: this.headers(),
method: "PATCH", method: "PATCH",
body: JSON.stringify({ body: JSON.stringify({
sha: sha, sha: sha,
}), }),
throw: false,
}); });
if (res.status < 200 || res.status >= 400) {
await this.logger.error("Failed to update branch head sha", res);
throw new Error(`Failed to update branch head sha, status ${res.status}`);
}
} }
/** /**
@ -126,7 +153,12 @@ export default class GithubClient {
const res = await requestUrl({ const res = await requestUrl({
url: url, url: url,
headers: this.headers(), headers: this.headers(),
throw: false,
}); });
if (res.status < 200 || res.status >= 400) {
await this.logger.error("Failed to get blob", res);
throw new Error(`Failed to get blob, status ${res.status}`);
}
return res.json; return res.json;
} }
@ -138,7 +170,7 @@ export default class GithubClient {
* @param content base64 encoded content of the file * @param content base64 encoded content of the file
*/ */
async createFile(path: string, content: string, message: string) { async createFile(path: string, content: string, message: string) {
await requestUrl({ const res = await requestUrl({
url: `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${path}`, url: `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${path}`,
headers: this.headers(), headers: this.headers(),
method: "PUT", method: "PUT",
@ -147,6 +179,11 @@ export default class GithubClient {
content: content, content: content,
branch: this.branch, branch: this.branch,
}), }),
throw: false,
}); });
if (res.status < 200 || res.status >= 400) {
await this.logger.error("Failed to create file", res);
throw new Error(`Failed to create file, status ${res.status}`);
}
} }
} }

53
src/logger.ts Normal file
View file

@ -0,0 +1,53 @@
import { Vault, normalizePath } from "obsidian";
const LOG_FILE_NAME = "github-sync.log" as const;
export default class Logger {
private enabled: boolean;
private logFile: string;
constructor(private vault: Vault) {
this.logFile = normalizePath(`${vault.configDir}/${LOG_FILE_NAME}`);
this.enabled = true;
}
private async write(
level: string,
message: string,
data?: any,
): Promise<void> {
if (!this.enabled) return;
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
additional_data: data,
};
await this.vault.adapter.append(
this.logFile,
JSON.stringify(logEntry) + "\n",
);
}
enable(): void {
this.enabled = true;
}
disable(): void {
this.enabled = false;
}
async info(message: string, data?: any): Promise<void> {
await this.write("INFO", message, data);
}
async warn(message: string, data?: any): Promise<void> {
await this.write("WARN", message, data);
}
async error(message: string, data?: any): Promise<void> {
await this.write("ERROR", message, data);
}
}

View file

@ -4,10 +4,12 @@ import GitHubSyncSettingsTab from "./settings/tab";
import SyncManager from "./sync-manager"; import SyncManager from "./sync-manager";
import { FileMetadata } from "./metadata-store"; import { FileMetadata } from "./metadata-store";
import { OnboardingDialog } from "./views/onboarding/view"; import { OnboardingDialog } from "./views/onboarding/view";
import Logger from "./logger";
export default class GitHubSyncPlugin extends Plugin { export default class GitHubSyncPlugin extends Plugin {
settings: GitHubSyncSettings; settings: GitHubSyncSettings;
syncManager: SyncManager; syncManager: SyncManager;
logger: Logger;
statusBarItem: HTMLElement | null = null; statusBarItem: HTMLElement | null = null;
uploadModifiedFilesRibbonIcon: HTMLElement | null = null; uploadModifiedFilesRibbonIcon: HTMLElement | null = null;
@ -27,6 +29,8 @@ export default class GitHubSyncPlugin extends Plugin {
} }
async onload() { async onload() {
this.logger = new Logger(this.app.vault);
await this.loadSettings(); await this.loadSettings();
this.addSettingTab(new GitHubSyncSettingsTab(this.app, this)); this.addSettingTab(new GitHubSyncSettingsTab(this.app, this));
@ -35,6 +39,7 @@ export default class GitHubSyncPlugin extends Plugin {
this.app.vault, this.app.vault,
this.settings, this.settings,
this.onConflicts.bind(this), this.onConflicts.bind(this),
this.logger,
); );
await this.syncManager.loadMetadata(); await this.syncManager.loadMetadata();

View file

@ -7,6 +7,7 @@ import GithubClient, {
import MetadataStore, { FileMetadata, Metadata } from "./metadata-store"; import MetadataStore, { FileMetadata, Metadata } from "./metadata-store";
import EventsListener from "./events-listener"; import EventsListener from "./events-listener";
import { GitHubSyncSettings } from "./settings/settings"; import { GitHubSyncSettings } from "./settings/settings";
import Logger from "./logger";
interface SyncAction { interface SyncAction {
type: "upload" | "download" | "delete_local" | "delete_remote"; type: "upload" | "download" | "delete_local" | "delete_remote";
@ -27,6 +28,7 @@ export default class SyncManager {
private vault: Vault, private vault: Vault,
private settings: GitHubSyncSettings, private settings: GitHubSyncSettings,
private onConflicts: OnConflictsCallback, private onConflicts: OnConflictsCallback,
private logger: Logger,
) { ) {
this.metadataStore = new MetadataStore(this.vault); this.metadataStore = new MetadataStore(this.vault);
this.client = new GithubClient( this.client = new GithubClient(
@ -34,11 +36,13 @@ export default class SyncManager {
this.settings.githubOwner, this.settings.githubOwner,
this.settings.githubRepo, this.settings.githubRepo,
this.settings.githubBranch, this.settings.githubBranch,
this.logger,
); );
this.eventsListener = new EventsListener( this.eventsListener = new EventsListener(
this.vault, this.vault,
this.metadataStore, this.metadataStore,
this.settings, this.settings,
this.logger,
); );
} }
@ -62,6 +66,7 @@ export default class SyncManager {
* This fails if neither remote nor local folders are empty. * This fails if neither remote nor local folders are empty.
*/ */
async firstSync() { async firstSync() {
await this.logger.info("Starting first sync");
let repositoryIsBare = false; let repositoryIsBare = false;
let res: RepoContent; let res: RepoContent;
let files: { let files: {
@ -81,6 +86,7 @@ export default class SyncManager {
} }
if (repositoryIsBare) { if (repositoryIsBare) {
await this.logger.info("Remote repository is bare");
// Since the repository is completely empty we need to create a first commit. // Since the repository is completely empty we need to create a first commit.
// We can't create that by going throught the normal sync process since the // We can't create that by going throught the normal sync process since the
// API doesn't let us create a new tree when the repo is empty. // API doesn't let us create a new tree when the repo is empty.
@ -103,6 +109,7 @@ export default class SyncManager {
if (!remoteRepoIsEmpty && !vaultIsEmpty) { if (!remoteRepoIsEmpty && !vaultIsEmpty) {
// Both have files, we can't sync, show error // Both have files, we can't sync, show error
await this.logger.error("Both remote and local have files, can't sync");
throw new Error("Both remote and local have files, can't sync"); throw new Error("Both remote and local have files, can't sync");
} else if (remoteRepoIsEmpty || repositoryIsBare) { } else if (remoteRepoIsEmpty || repositoryIsBare) {
// Remote has no files and no manifest, let's just upload whatever we have locally. // Remote has no files and no manifest, let's just upload whatever we have locally.
@ -130,6 +137,7 @@ export default class SyncManager {
files: { [key: string]: GetTreeResponseItem }, files: { [key: string]: GetTreeResponseItem },
treeSha: string, treeSha: string,
) { ) {
await this.logger.info("Starting first sync from remote files");
await Promise.all( await Promise.all(
Object.keys(files) Object.keys(files)
.filter((filePath: string) => { .filter((filePath: string) => {
@ -192,6 +200,7 @@ export default class SyncManager {
files: { [key: string]: GetTreeResponseItem }, files: { [key: string]: GetTreeResponseItem },
treeSha: string, treeSha: string,
) { ) {
await this.logger.info("Starting first sync from local files");
const newTreeFiles = Object.keys(files) const newTreeFiles = Object.keys(files)
.map((filePath: string) => ({ .map((filePath: string) => ({
path: files[filePath].path, path: files[filePath].path,
@ -228,10 +237,12 @@ export default class SyncManager {
* @returns * @returns
*/ */
async sync() { async sync() {
await this.logger.info("Starting sync");
const { files, sha: treeSha } = await this.client.getRepoContent(); const { files, sha: treeSha } = await this.client.getRepoContent();
const manifest = files[`${this.vault.configDir}/github-sync-metadata.json`]; const manifest = files[`${this.vault.configDir}/github-sync-metadata.json`];
if (manifest === undefined) { if (manifest === undefined) {
await this.logger.error("Remote manifest is missing", { files, treeSha });
throw new Error("Remote manifest is missing"); throw new Error("Remote manifest is missing");
} }
@ -250,6 +261,8 @@ export default class SyncManager {
// Add the new action to the list later on // Add the new action to the list later on
console.log("Conflicts"); console.log("Conflicts");
console.log(conflicts); console.log(conflicts);
await this.logger.warn("Found conflicts", conflicts);
(await this.onConflicts(conflicts)).forEach( (await this.onConflicts(conflicts)).forEach(
(resolution: boolean, index: number) => { (resolution: boolean, index: number) => {
if (resolution) { if (resolution) {
@ -277,8 +290,10 @@ export default class SyncManager {
if (actions.length === 0) { if (actions.length === 0) {
// Nothing to sync // Nothing to sync
await this.logger.info("Nothing to sync");
return; return;
} }
await this.logger.info("Actions to sync", actions);
const newTreeFiles: { [key: string]: NewTreeRequestItem } = Object.keys( const newTreeFiles: { [key: string]: NewTreeRequestItem } = Object.keys(
files, files,
@ -544,6 +559,7 @@ export default class SyncManager {
); );
await this.client.updateBranchHead(commitSha); await this.client.updateBranchHead(commitSha);
await this.logger.info("Sync done");
} }
async downloadFile(file: GetTreeResponseItem, lastModified: number) { async downloadFile(file: GetTreeResponseItem, lastModified: number) {
@ -585,8 +601,10 @@ export default class SyncManager {
} }
async loadMetadata() { async loadMetadata() {
await this.logger.info("Loading metadata");
await this.metadataStore.load(); await this.metadataStore.load();
if (Object.keys(this.metadataStore.data.files).length === 0) { if (Object.keys(this.metadataStore.data.files).length === 0) {
await this.logger.info("Metadata was empty, loading all files");
let files = []; let files = [];
let folders = [this.vault.getRoot().path]; let folders = [this.vault.getRoot().path];
while (folders.length > 0) { while (folders.length > 0) {
@ -595,6 +613,7 @@ export default class SyncManager {
continue; continue;
} }
if (!this.settings.syncConfigDir && folder === this.vault.configDir) { if (!this.settings.syncConfigDir && folder === this.vault.configDir) {
await this.logger.info("Skipping config dir");
// Skip the config dir if the user doesn't want to sync it // Skip the config dir if the user doesn't want to sync it
continue; continue;
} }
@ -630,6 +649,7 @@ export default class SyncManager {
}; };
this.metadataStore.save(); this.metadataStore.save();
} }
await this.logger.info("Loaded metadata");
} }
/** /**
@ -638,6 +658,7 @@ export default class SyncManager {
* as we need to add those files to the metadata store or they would never be synced. * as we need to add those files to the metadata store or they would never be synced.
*/ */
async addConfigDirToMetadata() { async addConfigDirToMetadata() {
await this.logger.info("Adding config dir to metadata");
// Get all the files in the config dir // Get all the files in the config dir
let files = []; let files = [];
let folders = [this.vault.configDir]; let folders = [this.vault.configDir];
@ -671,6 +692,7 @@ export default class SyncManager {
* keep being synced. * keep being synced.
*/ */
async removeConfigDirFromMetadata() { async removeConfigDirFromMetadata() {
await this.logger.info("Removing config dir from metadata");
// Get all the files in the config dir // Get all the files in the config dir
let files = []; let files = [];
let folders = [this.vault.configDir]; let folders = [this.vault.configDir];

View file

@ -306,6 +306,7 @@ const FirstSyncStepComponent = ({
async (_) => { async (_) => {
return []; return [];
}, },
plugin.logger,
); );
await syncManager.loadMetadata(); await syncManager.loadMetadata();
// When loading the plugin we also load the metadata, though since by default // When loading the plugin we also load the metadata, though since by default