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

View file

@ -1,4 +1,5 @@
import { requestUrl } from "obsidian";
import Logger from "src/logger";
export type RepoContent = {
files: { [key: string]: GetTreeResponseItem };
@ -43,6 +44,7 @@ export default class GithubClient {
private owner: string,
private repo: string,
private branch: string,
private logger: Logger,
) {}
headers() {
@ -62,7 +64,12 @@ export default class GithubClient {
const res = await requestUrl({
url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/trees/${this.branch}?recursive=1`,
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
.filter((file: GetTreeResponseItem) => file.type === "blob")
.reduce(
@ -81,7 +88,12 @@ export default class GithubClient {
headers: this.headers(),
method: "POST",
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;
}
@ -95,7 +107,12 @@ export default class GithubClient {
tree: treeSha,
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;
}
@ -103,19 +120,29 @@ export default class GithubClient {
const res = await requestUrl({
url: `https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`,
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;
}
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}`,
headers: this.headers(),
method: "PATCH",
body: JSON.stringify({
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({
url: url,
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;
}
@ -138,7 +170,7 @@ export default class GithubClient {
* @param content base64 encoded content of the file
*/
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}`,
headers: this.headers(),
method: "PUT",
@ -147,6 +179,11 @@ export default class GithubClient {
content: content,
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 { FileMetadata } from "./metadata-store";
import { OnboardingDialog } from "./views/onboarding/view";
import Logger from "./logger";
export default class GitHubSyncPlugin extends Plugin {
settings: GitHubSyncSettings;
syncManager: SyncManager;
logger: Logger;
statusBarItem: HTMLElement | null = null;
uploadModifiedFilesRibbonIcon: HTMLElement | null = null;
@ -27,6 +29,8 @@ export default class GitHubSyncPlugin extends Plugin {
}
async onload() {
this.logger = new Logger(this.app.vault);
await this.loadSettings();
this.addSettingTab(new GitHubSyncSettingsTab(this.app, this));
@ -35,6 +39,7 @@ export default class GitHubSyncPlugin extends Plugin {
this.app.vault,
this.settings,
this.onConflicts.bind(this),
this.logger,
);
await this.syncManager.loadMetadata();

View file

@ -7,6 +7,7 @@ import GithubClient, {
import MetadataStore, { FileMetadata, Metadata } from "./metadata-store";
import EventsListener from "./events-listener";
import { GitHubSyncSettings } from "./settings/settings";
import Logger from "./logger";
interface SyncAction {
type: "upload" | "download" | "delete_local" | "delete_remote";
@ -27,6 +28,7 @@ export default class SyncManager {
private vault: Vault,
private settings: GitHubSyncSettings,
private onConflicts: OnConflictsCallback,
private logger: Logger,
) {
this.metadataStore = new MetadataStore(this.vault);
this.client = new GithubClient(
@ -34,11 +36,13 @@ export default class SyncManager {
this.settings.githubOwner,
this.settings.githubRepo,
this.settings.githubBranch,
this.logger,
);
this.eventsListener = new EventsListener(
this.vault,
this.metadataStore,
this.settings,
this.logger,
);
}
@ -62,6 +66,7 @@ export default class SyncManager {
* This fails if neither remote nor local folders are empty.
*/
async firstSync() {
await this.logger.info("Starting first sync");
let repositoryIsBare = false;
let res: RepoContent;
let files: {
@ -81,6 +86,7 @@ export default class SyncManager {
}
if (repositoryIsBare) {
await this.logger.info("Remote repository is bare");
// 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
// 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) {
// 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");
} else if (remoteRepoIsEmpty || repositoryIsBare) {
// 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 },
treeSha: string,
) {
await this.logger.info("Starting first sync from remote files");
await Promise.all(
Object.keys(files)
.filter((filePath: string) => {
@ -192,6 +200,7 @@ export default class SyncManager {
files: { [key: string]: GetTreeResponseItem },
treeSha: string,
) {
await this.logger.info("Starting first sync from local files");
const newTreeFiles = Object.keys(files)
.map((filePath: string) => ({
path: files[filePath].path,
@ -228,10 +237,12 @@ export default class SyncManager {
* @returns
*/
async sync() {
await this.logger.info("Starting sync");
const { files, sha: treeSha } = await this.client.getRepoContent();
const manifest = files[`${this.vault.configDir}/github-sync-metadata.json`];
if (manifest === undefined) {
await this.logger.error("Remote manifest is missing", { files, treeSha });
throw new Error("Remote manifest is missing");
}
@ -250,6 +261,8 @@ export default class SyncManager {
// Add the new action to the list later on
console.log("Conflicts");
console.log(conflicts);
await this.logger.warn("Found conflicts", conflicts);
(await this.onConflicts(conflicts)).forEach(
(resolution: boolean, index: number) => {
if (resolution) {
@ -277,8 +290,10 @@ export default class SyncManager {
if (actions.length === 0) {
// Nothing to sync
await this.logger.info("Nothing to sync");
return;
}
await this.logger.info("Actions to sync", actions);
const newTreeFiles: { [key: string]: NewTreeRequestItem } = Object.keys(
files,
@ -544,6 +559,7 @@ export default class SyncManager {
);
await this.client.updateBranchHead(commitSha);
await this.logger.info("Sync done");
}
async downloadFile(file: GetTreeResponseItem, lastModified: number) {
@ -585,8 +601,10 @@ export default class SyncManager {
}
async loadMetadata() {
await this.logger.info("Loading metadata");
await this.metadataStore.load();
if (Object.keys(this.metadataStore.data.files).length === 0) {
await this.logger.info("Metadata was empty, loading all files");
let files = [];
let folders = [this.vault.getRoot().path];
while (folders.length > 0) {
@ -595,6 +613,7 @@ export default class SyncManager {
continue;
}
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
continue;
}
@ -630,6 +649,7 @@ export default class SyncManager {
};
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.
*/
async addConfigDirToMetadata() {
await this.logger.info("Adding config dir to metadata");
// Get all the files in the config dir
let files = [];
let folders = [this.vault.configDir];
@ -671,6 +692,7 @@ export default class SyncManager {
* keep being synced.
*/
async removeConfigDirFromMetadata() {
await this.logger.info("Removing config dir from metadata");
// Get all the files in the config dir
let files = [];
let folders = [this.vault.configDir];

View file

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