mirror of
https://github.com/silvanocerza/github-gitless-sync.git
synced 2026-07-22 05:41:36 +00:00
Add logger
This commit is contained in:
parent
0ca0be97f6
commit
2c4ed3e671
6 changed files with 137 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
53
src/logger.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue