482 lines
15 KiB
TypeScript
482 lines
15 KiB
TypeScript
import { Modal, Notice, Plugin, Setting } from "obsidian";
|
|
import cloneDeep from "lodash/cloneDeep";
|
|
import type { RemotelySavePluginSettings } from "./baseTypes";
|
|
import {
|
|
COMMAND_CALLBACK,
|
|
COMMAND_CALLBACK_ONEDRIVE,
|
|
COMMAND_CALLBACK_DROPBOX,
|
|
COMMAND_URI,
|
|
} from "./baseTypes";
|
|
import { importQrCodeUri } from "./importExport";
|
|
import type { InternalDBs } from "./localdb";
|
|
import {
|
|
insertDeleteRecord,
|
|
insertRenameRecord,
|
|
insertSyncPlanRecord,
|
|
loadDeleteRenameHistoryTable,
|
|
prepareDBs,
|
|
} from "./localdb";
|
|
import { RemoteClient } from "./remote";
|
|
import {
|
|
DEFAULT_DROPBOX_CONFIG,
|
|
getAuthUrlAndVerifier as getAuthUrlAndVerifierDropbox,
|
|
sendAuthReq as sendAuthReqDropbox,
|
|
setConfigBySuccessfullAuthInplace as setConfigBySuccessfullAuthInplaceDropbox,
|
|
} from "./remoteForDropbox";
|
|
import {
|
|
AccessCodeResponseSuccessfulType,
|
|
DEFAULT_ONEDRIVE_CONFIG,
|
|
sendAuthReq as sendAuthReqOnedrive,
|
|
setConfigBySuccessfullAuthInplace as setConfigBySuccessfullAuthInplaceOnedrive,
|
|
} from "./remoteForOnedrive";
|
|
import { DEFAULT_S3_CONFIG } from "./remoteForS3";
|
|
import { DEFAULT_WEBDAV_CONFIG } from "./remoteForWebdav";
|
|
import { RemotelySaveSettingTab } from "./settings";
|
|
import type { SyncStatusType } from "./sync";
|
|
import { doActualSync, getSyncPlan, isPasswordOk } from "./sync";
|
|
|
|
import * as origLog from "loglevel";
|
|
const log = origLog.getLogger("rs-default");
|
|
|
|
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
|
s3: DEFAULT_S3_CONFIG,
|
|
webdav: DEFAULT_WEBDAV_CONFIG,
|
|
dropbox: DEFAULT_DROPBOX_CONFIG,
|
|
onedrive: DEFAULT_ONEDRIVE_CONFIG,
|
|
password: "",
|
|
serviceType: "s3",
|
|
currLogLevel: "info",
|
|
};
|
|
|
|
interface OAuth2Info {
|
|
verifier?: string;
|
|
helperModal?: Modal;
|
|
authDiv?: HTMLElement;
|
|
revokeDiv?: HTMLElement;
|
|
revokeAuthSetting?: Setting;
|
|
}
|
|
|
|
export default class RemotelySavePlugin extends Plugin {
|
|
settings: RemotelySavePluginSettings;
|
|
// cm: CodeMirror.Editor;
|
|
db: InternalDBs;
|
|
syncStatus: SyncStatusType;
|
|
oauth2Info: OAuth2Info;
|
|
currLogLevel: string;
|
|
|
|
async onload() {
|
|
log.info(`loading plugin ${this.manifest.id}`);
|
|
|
|
this.oauth2Info = {
|
|
verifier: "",
|
|
helperModal: undefined,
|
|
authDiv: undefined,
|
|
revokeDiv: undefined,
|
|
revokeAuthSetting: undefined,
|
|
}; // init
|
|
|
|
await this.loadSettings();
|
|
|
|
if (this.settings.currLogLevel !== undefined) {
|
|
log.setLevel(this.settings.currLogLevel as any);
|
|
}
|
|
|
|
await this.checkIfOauthExpires();
|
|
|
|
await this.prepareDB();
|
|
|
|
this.syncStatus = "idle";
|
|
|
|
this.registerEvent(
|
|
this.app.vault.on("delete", async (fileOrFolder) => {
|
|
await insertDeleteRecord(this.db, fileOrFolder);
|
|
})
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.vault.on("rename", async (fileOrFolder, oldPath) => {
|
|
await insertRenameRecord(this.db, fileOrFolder, oldPath);
|
|
})
|
|
);
|
|
|
|
this.registerObsidianProtocolHandler(COMMAND_URI, async (inputParams) => {
|
|
const parsed = importQrCodeUri(inputParams, this.app.vault.getName());
|
|
if (parsed.status === "error") {
|
|
new Notice(parsed.message);
|
|
} else {
|
|
const copied = cloneDeep(parsed.result);
|
|
// new Notice(JSON.stringify(copied))
|
|
this.settings = Object.assign({}, this.settings, copied);
|
|
this.saveSettings();
|
|
new Notice(
|
|
`New not-oauth2 settings for ${this.manifest.name} saved. Reopen the plugin Settings to the effect.`
|
|
);
|
|
}
|
|
});
|
|
|
|
this.registerObsidianProtocolHandler(
|
|
COMMAND_CALLBACK,
|
|
async (inputParams) => {
|
|
new Notice(
|
|
`Your uri call a callback that's not supported yet: ${JSON.stringify(
|
|
inputParams
|
|
)}`
|
|
);
|
|
}
|
|
);
|
|
|
|
this.registerObsidianProtocolHandler(
|
|
COMMAND_CALLBACK_DROPBOX,
|
|
async (inputParams) => {
|
|
if (inputParams.code !== undefined) {
|
|
if (this.oauth2Info.helperModal !== undefined) {
|
|
this.oauth2Info.helperModal.contentEl.empty();
|
|
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
|
text: "Connecting to Dropbox...",
|
|
});
|
|
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
|
text: "Please DO NOT close this modal.",
|
|
});
|
|
}
|
|
|
|
let authRes = await sendAuthReqDropbox(
|
|
this.settings.dropbox.clientID,
|
|
this.oauth2Info.verifier,
|
|
inputParams.code
|
|
);
|
|
|
|
const self = this;
|
|
setConfigBySuccessfullAuthInplaceDropbox(
|
|
this.settings.dropbox,
|
|
authRes,
|
|
() => self.saveSettings()
|
|
);
|
|
|
|
const client = new RemoteClient(
|
|
"dropbox",
|
|
undefined,
|
|
undefined,
|
|
this.settings.dropbox,
|
|
undefined,
|
|
this.app.vault.getName(),
|
|
() => self.saveSettings()
|
|
);
|
|
|
|
const username = await client.getUser();
|
|
this.settings.dropbox.username = username;
|
|
await this.saveSettings();
|
|
|
|
new Notice(`Good! We've connected to Dropbox as user ${username}!`);
|
|
|
|
this.oauth2Info.verifier = ""; // reset it
|
|
this.oauth2Info.helperModal?.close(); // close it
|
|
this.oauth2Info.helperModal = undefined;
|
|
|
|
this.oauth2Info.authDiv?.toggleClass(
|
|
"dropbox-auth-button-hide",
|
|
this.settings.dropbox.username !== ""
|
|
);
|
|
this.oauth2Info.authDiv = undefined;
|
|
|
|
this.oauth2Info.revokeAuthSetting?.setDesc(
|
|
`You've connected as user ${this.settings.dropbox.username}. If you want to disconnect, click this button.`
|
|
);
|
|
this.oauth2Info.revokeAuthSetting = undefined;
|
|
this.oauth2Info.revokeDiv?.toggleClass(
|
|
"dropbox-revoke-auth-button-hide",
|
|
this.settings.dropbox.username === ""
|
|
);
|
|
this.oauth2Info.revokeDiv = undefined;
|
|
} else {
|
|
new Notice(
|
|
"Something went wrong from response from Dropbox. Maybe you rejected the auth?"
|
|
);
|
|
throw Error(
|
|
`do not know how to deal with the callback: ${JSON.stringify(
|
|
inputParams
|
|
)}`
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
this.registerObsidianProtocolHandler(
|
|
COMMAND_CALLBACK_ONEDRIVE,
|
|
async (inputParams) => {
|
|
if (inputParams.code !== undefined) {
|
|
if (this.oauth2Info.helperModal !== undefined) {
|
|
this.oauth2Info.helperModal.contentEl.empty();
|
|
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
|
text: "Connecting to Onedrive...",
|
|
});
|
|
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
|
text: "Please DO NOT close this modal.",
|
|
});
|
|
}
|
|
|
|
let rsp = await sendAuthReqOnedrive(
|
|
this.settings.onedrive.clientID,
|
|
this.settings.onedrive.authority,
|
|
inputParams.code,
|
|
this.oauth2Info.verifier
|
|
);
|
|
|
|
if ((rsp as any).error !== undefined) {
|
|
throw Error(`${JSON.stringify(rsp)}`);
|
|
}
|
|
|
|
const self = this;
|
|
setConfigBySuccessfullAuthInplaceOnedrive(
|
|
this.settings.onedrive,
|
|
rsp as AccessCodeResponseSuccessfulType,
|
|
() => self.saveSettings()
|
|
);
|
|
|
|
const client = new RemoteClient(
|
|
"onedrive",
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
this.settings.onedrive,
|
|
this.app.vault.getName(),
|
|
() => self.saveSettings()
|
|
);
|
|
this.settings.onedrive.username = await client.getUser();
|
|
await this.saveSettings();
|
|
|
|
this.oauth2Info.verifier = ""; // reset it
|
|
this.oauth2Info.helperModal?.close(); // close it
|
|
this.oauth2Info.helperModal = undefined;
|
|
|
|
this.oauth2Info.authDiv?.toggleClass(
|
|
"onedrive-auth-button-hide",
|
|
this.settings.onedrive.username !== ""
|
|
);
|
|
this.oauth2Info.authDiv = undefined;
|
|
|
|
this.oauth2Info.revokeAuthSetting?.setDesc(
|
|
`You've connected as user ${this.settings.onedrive.username}. If you want to disconnect, click this button.`
|
|
);
|
|
this.oauth2Info.revokeAuthSetting = undefined;
|
|
this.oauth2Info.revokeDiv?.toggleClass(
|
|
"onedrive-revoke-auth-button-hide",
|
|
this.settings.onedrive.username === ""
|
|
);
|
|
this.oauth2Info.revokeDiv = undefined;
|
|
} else {
|
|
new Notice(
|
|
"Something went wrong from response from OneDrive. Maybe you rejected the auth?"
|
|
);
|
|
throw Error(
|
|
`do not know how to deal with the callback: ${JSON.stringify(
|
|
inputParams
|
|
)}`
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
this.addRibbonIcon("switch", "Remotely Save", async () => {
|
|
if (this.syncStatus !== "idle") {
|
|
new Notice(
|
|
`Remotely Save already running in stage ${this.syncStatus}!`
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
//log.info(`huh ${this.settings.password}`)
|
|
new Notice(
|
|
`1/7 Remotely Save Sync Preparing (${this.settings.serviceType})`
|
|
);
|
|
this.syncStatus = "preparing";
|
|
|
|
new Notice("2/7 Starting to fetch remote meta data.");
|
|
this.syncStatus = "getting_remote_meta";
|
|
const self = this;
|
|
const client = new RemoteClient(
|
|
this.settings.serviceType,
|
|
this.settings.s3,
|
|
this.settings.webdav,
|
|
this.settings.dropbox,
|
|
this.settings.onedrive,
|
|
this.app.vault.getName(),
|
|
() => self.saveSettings()
|
|
);
|
|
const remoteRsp = await client.listFromRemote();
|
|
// log.info(remoteRsp);
|
|
|
|
new Notice("3/7 Starting to fetch local meta data.");
|
|
this.syncStatus = "getting_local_meta";
|
|
const local = this.app.vault.getAllLoadedFiles();
|
|
const localHistory = await loadDeleteRenameHistoryTable(this.db);
|
|
// log.info(local);
|
|
// log.info(localHistory);
|
|
|
|
new Notice("4/7 Checking password correct or not.");
|
|
this.syncStatus = "checking_password";
|
|
const passwordCheckResult = await isPasswordOk(
|
|
remoteRsp.Contents,
|
|
this.settings.password
|
|
);
|
|
if (!passwordCheckResult.ok) {
|
|
new Notice("something goes wrong while checking password");
|
|
throw Error(passwordCheckResult.reason);
|
|
}
|
|
|
|
new Notice("5/7 Starting to generate sync plan.");
|
|
this.syncStatus = "generating_plan";
|
|
const syncPlan = await getSyncPlan(
|
|
remoteRsp.Contents,
|
|
local,
|
|
localHistory,
|
|
this.db,
|
|
client.serviceType,
|
|
this.settings.password
|
|
);
|
|
log.info(syncPlan.mixedStates); // for debugging
|
|
await insertSyncPlanRecord(this.db, syncPlan);
|
|
|
|
// The operations above are read only and kind of safe.
|
|
// The operations below begins to write or delete (!!!) something.
|
|
|
|
new Notice("6/7 Remotely Save Sync data exchanging!");
|
|
|
|
this.syncStatus = "syncing";
|
|
await doActualSync(
|
|
client,
|
|
this.db,
|
|
this.app.vault,
|
|
syncPlan,
|
|
this.settings.password
|
|
);
|
|
|
|
new Notice("7/7 Remotely Save finish!");
|
|
this.syncStatus = "finish";
|
|
this.syncStatus = "idle";
|
|
} catch (error) {
|
|
const msg = `Remotely Save error while ${this.syncStatus}`;
|
|
log.info(msg);
|
|
log.info(error);
|
|
new Notice(msg);
|
|
new Notice(error.message);
|
|
this.syncStatus = "idle";
|
|
}
|
|
});
|
|
|
|
this.addSettingTab(new RemotelySaveSettingTab(this.app, this));
|
|
|
|
// this.registerDomEvent(document, "click", (evt: MouseEvent) => {
|
|
// log.info("click", evt);
|
|
// });
|
|
|
|
// this.registerInterval(
|
|
// window.setInterval(() => log.info("setInterval"), 5 * 60 * 1000)
|
|
// );
|
|
}
|
|
|
|
onunload() {
|
|
log.info(`unloading plugin ${this.manifest.id}`);
|
|
this.destroyDBs();
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign(
|
|
{},
|
|
cloneDeep(DEFAULT_SETTINGS),
|
|
await this.loadData()
|
|
);
|
|
if (this.settings.dropbox.clientID === "") {
|
|
this.settings.dropbox.clientID = DEFAULT_SETTINGS.dropbox.clientID;
|
|
}
|
|
if (this.settings.onedrive.clientID === "") {
|
|
this.settings.onedrive.clientID = DEFAULT_SETTINGS.onedrive.clientID;
|
|
}
|
|
if (this.settings.onedrive.authority === "") {
|
|
this.settings.onedrive.authority = DEFAULT_SETTINGS.onedrive.authority;
|
|
}
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
|
|
async checkIfOauthExpires() {
|
|
let needSave: boolean = false;
|
|
const current = Date.now();
|
|
|
|
// fullfill old version settings
|
|
if (
|
|
this.settings.dropbox.refreshToken !== "" &&
|
|
this.settings.dropbox.credentialsShouldBeDeletedAtTime === undefined
|
|
) {
|
|
// It has a refreshToken, but not expire time.
|
|
// Likely to be a setting from old version.
|
|
// we set it to a month.
|
|
this.settings.dropbox.credentialsShouldBeDeletedAtTime =
|
|
current + 1000 * 60 * 60 * 24 * 30;
|
|
needSave = true;
|
|
}
|
|
if (
|
|
this.settings.onedrive.refreshToken !== "" &&
|
|
this.settings.onedrive.credentialsShouldBeDeletedAtTime === undefined
|
|
) {
|
|
this.settings.onedrive.credentialsShouldBeDeletedAtTime =
|
|
current + 1000 * 60 * 60 * 24 * 30;
|
|
needSave = true;
|
|
}
|
|
|
|
// check expired or not
|
|
let dropboxExpired = false;
|
|
if (
|
|
this.settings.dropbox.refreshToken !== "" &&
|
|
current >= this.settings.dropbox.credentialsShouldBeDeletedAtTime
|
|
) {
|
|
dropboxExpired = true;
|
|
this.settings.dropbox = cloneDeep(DEFAULT_DROPBOX_CONFIG);
|
|
needSave = true;
|
|
}
|
|
|
|
let onedriveExpired = false;
|
|
if (
|
|
this.settings.onedrive.refreshToken !== "" &&
|
|
current >= this.settings.onedrive.credentialsShouldBeDeletedAtTime
|
|
) {
|
|
onedriveExpired = true;
|
|
this.settings.onedrive = cloneDeep(DEFAULT_ONEDRIVE_CONFIG);
|
|
needSave = true;
|
|
}
|
|
|
|
// save back
|
|
if (needSave) {
|
|
await this.saveSettings();
|
|
}
|
|
|
|
// send notice
|
|
if (dropboxExpired && onedriveExpired) {
|
|
new Notice(
|
|
`${this.manifest.name}: You haven't manually auth Dropbox and OneDrive for a while, you need to re-auth them again.`,
|
|
6000
|
|
);
|
|
} else if (dropboxExpired) {
|
|
new Notice(
|
|
`${this.manifest.name}: You haven't manually auth Dropbox for a while, you need to re-auth it again.`,
|
|
6000
|
|
);
|
|
} else if (onedriveExpired) {
|
|
new Notice(
|
|
`${this.manifest.name}: You haven't manually auth OneDrive for a while, you need to re-auth it again.`,
|
|
6000
|
|
);
|
|
}
|
|
}
|
|
|
|
async prepareDB() {
|
|
this.db = await prepareDBs();
|
|
}
|
|
|
|
destroyDBs() {
|
|
/* destroyDBs(this.db); */
|
|
}
|
|
}
|