From 2e09686675d0fde21ea6f05ad0646936d85a8d91 Mon Sep 17 00:00:00 2001 From: fyears <1142836+fyears@users.noreply.github.com> Date: Tue, 11 Jun 2024 00:01:36 +0800 Subject: [PATCH] add yandex disk pro --- .env.example.txt | 2 + .github/workflows/auto-build.yml | 2 + .github/workflows/release.yml | 2 + README.md | 5 + docs/remote_services/yandexdisk/README.md | 44 ++ esbuild.config.mjs | 5 + pro/src/account.ts | 66 +-- pro/src/add_new_service.md | 17 + pro/src/baseTypesPro.ts | 23 +- pro/src/fsBox.ts | 4 - pro/src/fsYandexDisk.ts | 516 ++++++++++++++++++++++ pro/src/langs/en.json | 36 ++ pro/src/langs/zh_cn.json | 40 ++ pro/src/langs/zh_tw.json | 40 ++ pro/src/settingsPro.ts | 23 +- pro/src/settingsYandexDisk.ts | 378 ++++++++++++++++ pro/src/yandexApi.ts | 307 +++++++++++++ src/baseTypes.ts | 11 +- src/fsGetter.ts | 7 + src/importExport.ts | 3 + src/main.ts | 83 ++++ src/settings.ts | 41 +- styles.css | 19 + tests/configPersist.test.ts | 3 + webpack.config.js | 5 + 25 files changed, 1638 insertions(+), 44 deletions(-) create mode 100644 docs/remote_services/yandexdisk/README.md create mode 100644 pro/src/add_new_service.md create mode 100644 pro/src/fsYandexDisk.ts create mode 100644 pro/src/settingsYandexDisk.ts create mode 100644 pro/src/yandexApi.ts diff --git a/.env.example.txt b/.env.example.txt index 7fed9db..ee851bf 100644 --- a/.env.example.txt +++ b/.env.example.txt @@ -9,3 +9,5 @@ BOX_CLIENT_ID= BOX_CLIENT_SECRET= PCLOUD_CLIENT_ID= PCLOUD_CLIENT_SECRET= +YANDEXDISK_CLIENT_ID= +YANDEXDISK_CLIENT_SECRET= diff --git a/.github/workflows/auto-build.yml b/.github/workflows/auto-build.yml index cb11b26..82e4bc4 100644 --- a/.github/workflows/auto-build.yml +++ b/.github/workflows/auto-build.yml @@ -27,6 +27,8 @@ jobs: BOX_CLIENT_SECRET: ${{secrets.BOX_CLIENT_SECRET}} PCLOUD_CLIENT_ID: ${{secrets.PCLOUD_CLIENT_ID}} PCLOUD_CLIENT_SECRET: ${{secrets.PCLOUD_CLIENT_SECRET}} + YANDEXDISK_CLIENT_ID: ${{secrets.YANDEXDISK_CLIENT_ID}} + YANDEXDISK_CLIENT_SECRET: ${{secrets.YANDEXDISK_CLIENT_SECRET}} strategy: matrix: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56cf654..09080f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,8 @@ jobs: BOX_CLIENT_SECRET: ${{secrets.BOX_CLIENT_SECRET}} PCLOUD_CLIENT_ID: ${{secrets.PCLOUD_CLIENT_ID}} PCLOUD_CLIENT_SECRET: ${{secrets.PCLOUD_CLIENT_SECRET}} + YANDEXDISK_CLIENT_ID: ${{secrets.YANDEXDISK_CLIENT_ID}} + YANDEXDISK_CLIENT_SECRET: ${{secrets.YANDEXDISK_CLIENT_SECRET}} strategy: matrix: diff --git a/README.md b/README.md index 1e9282a..e360be1 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find - Google Drive (PRO feature) - Box (PRO feature) - pCloud (PRO feature) + - Yandex Disk (PRO feature) - [Here](./docs/services_connectable_or_not.md) shows more connectable (or not-connectable) services in details. - **Obsidian Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker". - **[End-to-end encryption](./docs/encryption/README.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password. @@ -134,6 +135,10 @@ PRO (paid) feature "sync with Box" allows users to to sync with Box. Tutorials a PRO (paid) feature "sync with pCloud" allows users to to sync with pCloud (using its native API instead of webdav). Tutorials and limitations are documented [here](./docs/remote_services/pcloud/README.md). +### Yandex Disk (PRO feature) + +PRO (paid) feature "sync with Yandex Disk" allows users to to sync with Yandex Disk (using its native API instead of webdav). Tutorials and limitations are documented [here](./docs/remote_services/yandexdisk/README.md). + ## Scheduled Auto Sync - You can configure auto syncing every N minutes in settings. diff --git a/docs/remote_services/yandexdisk/README.md b/docs/remote_services/yandexdisk/README.md new file mode 100644 index 0000000..f5fa648 --- /dev/null +++ b/docs/remote_services/yandexdisk/README.md @@ -0,0 +1,44 @@ +# Yandex Disk (PRO) + +# Links + + + +# Intro + +* It's a PRO feature of Remotely Save plugin. +* **This plugin is NOT an official Yandex Disk or Yandex product, and just uses Yandex Disk's public api.** + +# Steps + +## Steps of Remotely Save subscription + +1. Please sign up and sign in an online account, connect your plugin to your online account firstly. See [the PRO tutorial](../../pro/README.md) firstly. +2. Subscribe to "sync with Yandex Disk" feature online. +3. Go back to your Remotely Save plugin inside Obsidian, click "Check again" button in PRO settings. So that the plugin knows some features are enabled. In this case, sync with Yandex Disk should be detected. + +## Steps of Connecting to your Yandex Disk + +After you enabled the PRO feature in your Remotely Save plugin, you can connect to your Yandex Disk account now. + +1. In Remotely Save settings, change your sync service to Yandex Disk. +2. Click Auth, visit the link, go to Yandex Disk website to start. +3. Follow the instruction on Yandex Disk, and allow Remotely Save to connect. +4. You will be redirected back to Remotely Save plugin. +5. A notice will tell you that you've connected or not. +6. Sync! The plugin will create a vault folder in the root of your Yandex Disk and upload notes into that folder. +7. **Read the caveats below.** + +# The caveats + +* As of June 2024, this feature is in beta stage. **Back up your vault before using this feature.** + +# Why not use webdav? + +1. The Remotely Save PRO feture "sync with Yandex Disk" is developed using Yandex Disk's native API, instead of webdav interface. It brings benefits such that the last modified time can be preserved. +2. Some users prefer oauth2 method to authorize themselves. + +# Bonus: where to register the app + +* or +* redirect uri can be set to or . diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 22537d3..415c64f 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -26,6 +26,9 @@ const DEFAULT_BOX_CLIENT_ID = process.env.BOX_CLIENT_ID || ""; const DEFAULT_BOX_CLIENT_SECRET = process.env.BOX_CLIENT_SECRET || ""; const DEFAULT_PCLOUD_CLIENT_ID = process.env.PCLOUD_CLIENT_ID || ""; const DEFAULT_PCLOUD_CLIENT_SECRET = process.env.PCLOUD_CLIENT_SECRET || ""; +const DEFAULT_YANDEXDISK_CLIENT_ID = process.env.YANDEXDISK_CLIENT_ID || ""; +const DEFAULT_YANDEXDISK_CLIENT_SECRET = + process.env.YANDEXDISK_CLIENT_SECRET || ""; esbuild .context({ @@ -69,6 +72,8 @@ esbuild "process.env.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`, "process.env.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`, "process.env.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`, + "process.env.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`, + "process.env.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`, global: "window", "process.env.NODE_DEBUG": `undefined`, // ugly fix "process.env.DEBUG": `undefined`, // ugly fix diff --git a/pro/src/account.ts b/pro/src/account.ts index 7520c8c..bb08a77 100644 --- a/pro/src/account.ts +++ b/pro/src/account.ts @@ -2,6 +2,7 @@ import { nanoid } from "nanoid"; import { OAUTH2_FORCE_EXPIRE_MILLISECONDS, type RemotelySavePluginSettings, + type SUPPORTED_SERVICES_TYPE, } from "../../src/baseTypes"; import { COMMAND_CALLBACK_PRO, @@ -267,42 +268,41 @@ export const checkProRunnableAndFixInplace = async ( // good to go } - // check for google_drive - console.debug( - `checking "feature-google_drive", serviceType=${config.serviceType}` - ); - if (config.serviceType === "googledrive") { - if ( - config.pro.enabledProFeatures.filter( - (x) => x.featureName === "feature-google_drive" - ).length === 1 - ) { - // good to go - } else { - errorMsgs.push( - `You're trying to use "sync with Google Drive" PRO feature but you haven't subscribe to it.` - ); - } - } else { - // good to go - } + const toChecked: { + feature: PRO_FEATURE_TYPE; + service: SUPPORTED_SERVICES_TYPE; + name: string; + }[] = [ + { + feature: "feature-google_drive", + service: "googledrive", + name: "Google Drive", + }, + { feature: "feature-box", service: "box", name: "Box" }, + { feature: "feature-pcloud", service: "pcloud", name: "pCloud" }, + { + feature: "feature-yandex_disk", + service: "yandexdisk", + name: "Yandex Disk", + }, + ]; - // check for box - console.debug(`checking "feature-box", serviceType=${config.serviceType}`); - if (config.serviceType === "box") { - if ( - config.pro.enabledProFeatures.filter( - (x) => x.featureName === "feature-box" - ).length === 1 - ) { - // good to go + for (const { feature, service, name } of toChecked) { + console.debug(`checking "${feature}", serviceType=${config.serviceType}`); + if (config.serviceType === service) { + if ( + config.pro.enabledProFeatures.filter((x) => x.featureName === feature) + .length === 1 + ) { + // good to go + } else { + errorMsgs.push( + `You're trying to use "sync with ${name}" PRO feature but you haven't subscribe to it.` + ); + } } else { - errorMsgs.push( - `You're trying to use "sync with Box" PRO feature but you haven't subscribe to it.` - ); + // good to go } - } else { - // good to go } if (errorMsgs.length !== 0) { diff --git a/pro/src/add_new_service.md b/pro/src/add_new_service.md new file mode 100644 index 0000000..1069ce3 --- /dev/null +++ b/pro/src/add_new_service.md @@ -0,0 +1,17 @@ +# checklist for adding new service + +1. `fsXxx.ts` +2. `settingsXxx.ts` +3. add callback and `xxxConfig` in `baseTypesPro.ts` +4. add `xxxConfig` to `DEFAULT_SETTINGS` and `RemotelySavePluginSettings` in `main.ts` +5. add `registerObsidianProtocolHandler`, if undefinded, `let xxxExpired`, expired notice in `main.ts` +6. add `langs/` +7. add css into `styles.css` +8. general `settings.ts` add: config part, chooser, import export +9. `generateProSettingsPart.ts` +10. `importExport.ts` +11. `fsGetter.ts` +12. `sync.ts` checking for PRO: `checkProRunnableAndFixInplace` +13. `configPersist.test.ts` +14. `README.md` add service at menu and detail +15. `docs/` add service diff --git a/pro/src/baseTypesPro.ts b/pro/src/baseTypesPro.ts index 5025db6..d882422 100644 --- a/pro/src/baseTypesPro.ts +++ b/pro/src/baseTypesPro.ts @@ -10,7 +10,8 @@ export type PRO_FEATURE_TYPE = | "feature-smart_conflict" | "feature-google_drive" | "feature-box" - | "feature-pcloud"; + | "feature-pcloud" + | "feature-yandex_disk"; export interface FeatureInfo { featureName: PRO_FEATURE_TYPE; @@ -89,3 +90,23 @@ export interface PCloudConfig { emptyFile: "skip" | "error"; kind: "pcloud"; } + +/////////////////////////////////////////////////////////// +// Yandex Disk +////////////////////////////////////////////////////////// + +export const COMMAND_CALLBACK_YANDEXDISK = "remotely-save-cb-yandexdisk"; +export const YANDEXDISK_CLIENT_ID = process.env.DEFAULT_YANDEXDISK_CLIENT_ID; +export const YANDEXDISK_CLIENT_SECRET = + process.env.DEFAULT_YANDEXDISK_CLIENT_SECRET; + +export interface YandexDiskConfig { + accessToken: string; + accessTokenExpiresInMs: number; + accessTokenExpiresAtTimeMs: number; + refreshToken: string; + remoteBaseDir?: string; + credentialsShouldBeDeletedAtTimeMs?: number; + scope: string; + kind: "yandexdisk"; +} diff --git a/pro/src/fsBox.ts b/pro/src/fsBox.ts index 3756883..44c1570 100644 --- a/pro/src/fsBox.ts +++ b/pro/src/fsBox.ts @@ -407,10 +407,6 @@ export class FakeFsBox extends FakeFs { await this.saveUpdatedConfigFunc(); console.info("Box accessToken updated"); return this.boxConfig.accessToken; - - // const access = "tUf643YKLuGhUmXRBKPAK0hz9ZKv85kS"; - // this.boxConfig.accessToken = access; - // return access; } async walk(): Promise { diff --git a/pro/src/fsYandexDisk.ts b/pro/src/fsYandexDisk.ts new file mode 100644 index 0000000..e6fe435 --- /dev/null +++ b/pro/src/fsYandexDisk.ts @@ -0,0 +1,516 @@ +import { nanoid } from "nanoid"; +import PQueue from "p-queue"; +import type { Entity } from "../../src/baseTypes"; +import { FakeFs } from "../../src/fsAll"; +import { unixTimeToStr } from "../../src/misc"; +import { + COMMAND_CALLBACK_YANDEXDISK, + YANDEXDISK_CLIENT_ID, + YANDEXDISK_CLIENT_SECRET, + type YandexDiskConfig, +} from "./baseTypesPro"; +import { + type FilesResourceList, + type Resource, + type ResourceList, + YandexApi, +} from "./yandexApi"; + +export const DEFAULT_YANDEXDISK_CONFIG: YandexDiskConfig = { + accessToken: "", + accessTokenExpiresInMs: 0, + accessTokenExpiresAtTimeMs: 0, + refreshToken: "", + scope: "", + kind: "yandexdisk", +}; + +/** + * https://yandex.com/dev/id/doc/en/codes/code-url#code + */ +export const generateAuthUrl = (hasCallback: boolean) => { + let callback = `https://oauth.yandex.com/verification_code`; + if (hasCallback) { + callback = `obsidian://${COMMAND_CALLBACK_YANDEXDISK}`; + } + + const params = new URLSearchParams({ + response_type: "code", + client_id: YANDEXDISK_CLIENT_ID ?? "", + redirect_uri: callback, + force_confirm: "yes", + state: nanoid(), + }); + + const url = `https://oauth.yandex.com/authorize?${params}`; + return url; +}; + +interface AuthResSucc { + token_type: "bearer"; + access_token: string; + expires_in: number; + refresh_token: string; + scope: string | undefined; +} + +interface AuthResFail { + error: string; + error_description: string; +} + +/** + * https://yandex.com/dev/id/doc/en/codes/code-url#token + */ +export const sendAuthReq = async (authCode: string, errorCallBack: any) => { + try { + const k = { + code: authCode, + grant_type: "authorization_code", + client_id: YANDEXDISK_CLIENT_ID ?? "", + client_secret: YANDEXDISK_CLIENT_SECRET ?? "", + // redirect_uri: `obsidian://${COMMAND_CALLBACK_BOX}`, + }; + // console.debug(k); + const resp1 = await fetch(`https://oauth.yandex.com/token`, { + method: "POST", + body: new URLSearchParams(k), + }); + + if (resp1.status !== 200) { + const resp2: AuthResFail = await resp1.json(); + throw Error(JSON.stringify(resp2)); + } + + const resp2: AuthResSucc = await resp1.json(); + return resp2; + } catch (e) { + console.error(e); + if (errorCallBack !== undefined) { + await errorCallBack(e); + } + } +}; + +/** + * https://yandex.com/dev/id/doc/en/tokens/refresh-client + */ +export const sendRefreshTokenReq = async (refreshToken: string) => { + console.debug(`refreshing token`); + const x = await fetch("https://oauth.yandex.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: YANDEXDISK_CLIENT_ID ?? "", + client_secret: YANDEXDISK_CLIENT_SECRET ?? "", + grant_type: "refresh_token", + refresh_token: refreshToken, + }).toString(), + }); + + if (x.status === 200) { + const y: AuthResSucc = await x.json(); + console.debug(`new token obtained`); + return y; + } else { + const y: AuthResFail = await x.json(); + throw Error(`cannot refresh an access token: ${JSON.stringify(y)}`); + } +}; + +export const setConfigBySuccessfullAuthInplace = async ( + config: YandexDiskConfig, + authRes: any, + saveUpdatedConfigFunc: () => Promise | undefined +) => { + if (authRes.access_token === undefined || authRes.access_token === "") { + throw Error( + `you should not save the setting for ${JSON.stringify(authRes)}` + ); + } + + config.accessToken = authRes.access_token; + config.accessTokenExpiresAtTimeMs = + Date.now() + authRes.expires_in * 1000 - 5 * 60 * 1000; + config.accessTokenExpiresInMs = authRes.expires_in * 1000; + config.refreshToken = authRes.refresh_token || config.refreshToken; + + // manually set it expired after 60 days; + config.credentialsShouldBeDeletedAtTimeMs = + Date.now() + 1000 * 60 * 60 * 24 * 59; + + await saveUpdatedConfigFunc?.(); + + console.info("finish updating local info of Yandex Disk token"); +}; + +const getYandexDiskPath = (fileOrFolderPath: string, remoteBaseDir: string) => { + let key = fileOrFolderPath; + if (fileOrFolderPath === "/" || fileOrFolderPath === "") { + // special + key = `disk:/${remoteBaseDir}`; + } else if (fileOrFolderPath.startsWith("/")) { + console.warn( + `why the path ${fileOrFolderPath} starts with '/'? but we just go on.` + ); + key = `disk:/${remoteBaseDir}${fileOrFolderPath}`; + } else { + key = `disk:/${remoteBaseDir}/${fileOrFolderPath}`; + } + if (key.endsWith("/")) { + key = key.slice(0, key.length - 1); + } + return key; +}; + +const getNormPath = ( + fileOrFolderPath: string, + remoteBaseDir: string, + type: "dir" | "file" +) => { + if ( + !( + fileOrFolderPath === `disk:/${remoteBaseDir}` || + fileOrFolderPath.startsWith(`disk:/${remoteBaseDir}/`) + ) + ) { + throw Error( + `"${fileOrFolderPath}" doesn't starts with "disk:/${remoteBaseDir}/"` + ); + } + let key = fileOrFolderPath.slice(`disk:/${remoteBaseDir}/`.length); + if (type === "dir") { + key = `${key}/`; + } + return key; +}; + +const fromResourceToEntity = (x: Resource, remoteBaseDir: string): Entity => { + const key = getNormPath(x.path!, remoteBaseDir, x.type!); + if (x.type === "dir") { + return { + key: key, + keyRaw: key, + size: 0, + sizeRaw: 0, + } as Entity; + } else { + // file + const mtimeCli = Date.parse( + x?.custom_properties?.rclone_modified ?? x.modified! + ).valueOf(); + const mtimeSvr = Date.parse(x.modified!).valueOf(); + return { + key: key, + keyRaw: key, + mtimeCli: mtimeCli, + mtimeSvr: mtimeSvr, + size: x.size!, + sizeRaw: x.size!, + hash: x.sha256, + } as Entity; + } +}; + +const FIELDS_FOR_RESOURCE = [ + "name", + "created", + "modified", + "path", + "type", + "size", + "sha256", + "md5", + "_embedded.limit", + "_embedded.offset", + "_embedded.total", + "_embedded.items.created", + "_embedded.items.modified", + "_embedded.items.path", + "_embedded.items.name", + "_embedded.items.type", + "_embedded.items.size", + "_embedded.items.sha256", + "_embedded.items.md5", + "_embedded.items.mime_type", + "_embedded.items.file", + "_embedded.items.custom_properties", +]; + +export class FakeFsYandexDisk extends FakeFs { + kind: string; + yandexDiskConfig: YandexDiskConfig; + remoteBaseDir: string; + vaultFolderExists: boolean; + saveUpdatedConfigFunc: () => Promise; + + constructor( + yandexDiskConfig: YandexDiskConfig, + vaultName: string, + saveUpdatedConfigFunc: () => Promise + ) { + super(); + this.kind = "yandexdisk"; + this.yandexDiskConfig = yandexDiskConfig; + this.remoteBaseDir = this.yandexDiskConfig.remoteBaseDir || vaultName || ""; + this.vaultFolderExists = false; + this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; + } + + async _init() { + const access = await this._getAccessToken(); + const client = new YandexApi(access); + + if (this.vaultFolderExists) { + // pass + } else { + const res: Resource[] = []; + let offset = 0; + const limit = 100; + let total = 100; + do { + const k = await client.diskResources( + "disk:/", + FIELDS_FOR_RESOURCE, + limit, + offset + ); + res.push(...(k._embedded?.items ?? [])); + total = k._embedded?.total ?? 0; + offset += limit; + } while (offset < total); + + if (res.filter((x) => x.name === this.remoteBaseDir).length > 0) { + // found + this.vaultFolderExists = true; + } else { + // need to create + await client.diskResourcesPut(`disk:/${this.remoteBaseDir}`); + this.vaultFolderExists = true; + } + } + } + + async _getAccessToken() { + if (this.yandexDiskConfig.refreshToken === "") { + throw Error("The user has not manually auth yet."); + } + + const ts = Date.now(); + const comp = this.yandexDiskConfig.accessTokenExpiresAtTimeMs > ts; + // console.debug(`this.yandexDiskConfig.accessTokenExpiresAtTimeMs=${this.yandexDiskConfig.accessTokenExpiresAtTimeMs},ts=${ts},comp=${comp}`) + if (comp) { + return this.yandexDiskConfig.accessToken; + } + + // refresh + const k = await sendRefreshTokenReq(this.yandexDiskConfig.refreshToken); + this.yandexDiskConfig.accessToken = k.access_token; + this.yandexDiskConfig.accessTokenExpiresInMs = k.expires_in * 1000; + this.yandexDiskConfig.accessTokenExpiresAtTimeMs = + ts + k.expires_in * 1000 - 60 * 2 * 1000; + await this.saveUpdatedConfigFunc(); + console.info("Yandex Disk accessToken updated"); + return this.yandexDiskConfig.accessToken; + } + + async walk(): Promise { + await this._init(); + + // bfs + const queue = new PQueue({ + concurrency: 5, // TODO: make it configurable? + autoStart: true, + }); + queue.on("error", (error) => { + queue.pause(); + queue.clear(); + throw error; + }); + + const entities: Entity[] = []; + + let parents = ["/"]; + while (parents.length !== 0) { + const children: typeof parents = []; + for (const p of parents) { + queue.add(async () => { + const entitiesOfALevel = await this._walkFolder(p); + for (const entity of entitiesOfALevel) { + entities.push(entity); + if (entity.keyRaw.endsWith("/")) { + children.push(entity.keyRaw); + } + } + }); + } + await queue.onIdle(); + parents = children; + } + + // console.debug(entities); + return entities; + } + + async walkPartial(): Promise { + await this._init(); + const entities = await this._walkFolder("/"); + return entities; + } + + async _walkFolder(key: string) { + if (!key.endsWith("/")) { + throw Error(`should not call _walkFolder on ${key}`); + } + const client = new YandexApi(await this._getAccessToken()); + const p = getYandexDiskPath(key, this.remoteBaseDir); + const entities: Entity[] = []; + let offset = 0; + const limit = 100; + let total = 100; + // TODO: once we know the total in the first loop, we can run the list in parallel + do { + const k = await client.diskResources( + p, + FIELDS_FOR_RESOURCE, + limit, + offset + ); + entities.push( + ...(k._embedded?.items ?? []).map((x) => + fromResourceToEntity(x, this.remoteBaseDir) + ) + ); + total = k._embedded?.total ?? 0; + offset += limit; + } while (offset < total); + return entities; + } + + async stat(key: string): Promise { + await this._init(); + const client = new YandexApi(await this._getAccessToken()); + const p = getYandexDiskPath(key, this.remoteBaseDir); + const r = await client.diskResources(p, FIELDS_FOR_RESOURCE); + const entity = fromResourceToEntity(r, this.remoteBaseDir); + return entity; + } + + async mkdir( + key: string, + mtime?: number | undefined, + ctime?: number | undefined + ): Promise { + // console.debug(`mkdir ${key} begin`) + await this._init(); + const client = new YandexApi(await this._getAccessToken()); + const p = getYandexDiskPath(key, this.remoteBaseDir); + // create + await client.diskResourcesPut(p, FIELDS_FOR_RESOURCE); + // patch? + const custom: Record = {}; + if (mtime !== undefined) { + custom["rclone_modified"] = unixTimeToStr(mtime, true); + } + if (ctime !== undefined) { + custom["rclone_created"] = unixTimeToStr(ctime, true); + } + if (Object.keys(custom).length > 0) { + await client.diskResourcesPatch(p, custom); + } + const entity = await this.stat(key); + // console.debug(`mkdir ${key} finish, ${JSON.stringify(entity)}`) + return entity; + } + + async writeFile( + key: string, + content: ArrayBuffer, + mtime: number, + ctime: number + ): Promise { + // console.debug(`writeFile ${key} begin`) + await this._init(); + const client = new YandexApi(await this._getAccessToken()); + const p = getYandexDiskPath(key, this.remoteBaseDir); + await client.diskResoucesUpload(p, content, true); + // console.debug(`writeFile ${key} upload succ`) + // patch? + const custom: Record = {}; + if (mtime !== undefined) { + custom["rclone_modified"] = unixTimeToStr(mtime, true); + } + if (ctime !== undefined) { + custom["rclone_created"] = unixTimeToStr(ctime, true); + } + if (Object.keys(custom).length > 0) { + await client.diskResourcesPatch(p, custom); + } + // console.debug(`writeFile ${key} patch succ`) + const entity = await this.stat(key); + // console.debug(`writeFile ${key} finish, ${JSON.stringify(entity)}`) + return entity; + } + + async readFile(key: string): Promise { + // console.debug(`readFile ${key} begin`) + await this._init(); + const client = new YandexApi(await this._getAccessToken()); + const p = getYandexDiskPath(key, this.remoteBaseDir); + const content = await client.diskResoucesDownload(p); + // console.debug(`readFile ${key} finish, length=${content.byteLength}`) + return content; + } + + async rename(key1: string, key2: string): Promise { + await this._init(); + throw new Error("Method not implemented."); + } + + async rm(key: string): Promise { + await this._init(); + const client = new YandexApi(await this._getAccessToken()); + const p = getYandexDiskPath(key, this.remoteBaseDir); + await client.diskResourcesDelete(p, false); + } + + async checkConnect(callbackFunc?: any): Promise { + // if we can init, we can connect + try { + await this._init(); + return true; + } catch (err) { + console.debug(err); + callbackFunc?.(err); + return false; + } + } + + async getUserDisplayName(): Promise { + await this._init(); + throw new Error("Method not implemented."); + } + + /** + * https://yandex.com/dev/id/doc/en/tokens/token-invalidate + */ + async revokeAuth(): Promise { + await fetch(`https://oauth.yandex.com/revoke_token`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: YANDEXDISK_CLIENT_ID ?? "", + client_secret: YANDEXDISK_CLIENT_SECRET ?? "", + access_token: this.yandexDiskConfig.refreshToken, // TODO: which token? + }).toString(), + }); + } + + allowEmptyFile(): boolean { + return true; + } +} diff --git a/pro/src/langs/en.json b/pro/src/langs/en.json index dce5fdc..6f8078d 100644 --- a/pro/src/langs/en.json +++ b/pro/src/langs/en.json @@ -17,6 +17,11 @@ "protocol_pcloud_connect_fail": "Something went wrong from response from pCloud website. Maybe the network connection is not good. Maybe you rejected the auth?", "protocol_pcloud_connect_succ_revoke": "You've connected. If you want to disconnect, click this button.", + "protocol_yandexdisk_connecting": "Connectting", + "protocol_yandexdisk_connect_manualinput_succ": "You've connected", + "protocol_yandexdisk_connect_fail": "Something went wrong from response from Yandex Disk website. Maybe the network connection is not good. Maybe you rejected the auth?", + "protocol_yandexdisk_connect_succ_revoke": "You've connected. If you want to disconnect, click this button.", + "modal_googledriveauth_tutorial": "

Please firstly go to the address, then go on the auth flow. In the end, you will see a code, please paste that code here and submit.

", "modal_googledriveauth_copybutton": "Click to copy the auth url", "modal_googledriveauth_copynotice": "The auth url is copied to the clipboard!", @@ -60,6 +65,17 @@ "modal_pcloudrevokeauth_clean_notice": "Cleaned!", "modal_pcloudrevokeauth_clean_fail": "Something goes wrong while revoking.", + "modal_yandexdiskauth_tutorial": "

Please firstly go to the address, then go on the auth flow. In the end, you will be redirected to here.

", + "modal_yandexdiskauth_copybutton": "Click to copy the auth url", + "modal_yandexdiskauth_copynotice": "The auth url is copied to the clipboard!", + "modal_yandexdiskrevokeauth_step1": "Step 1: Go to the following address, you can remove the connection there.", + "modal_yandexdiskrevokeauth_step2": "Step 2: Click the button below, to clean the locally-saved login credentials.", + "modal_yandexdiskrevokeauth_clean": "Clean Locally-Saved Login Credentials", + "modal_yandexdiskrevokeauth_clean_desc": "You need to click the button.", + "modal_yandexdiskrevokeauth_clean_button": "Clean", + "modal_yandexdiskrevokeauth_clean_notice": "Cleaned!", + "modal_yandexdiskrevokeauth_clean_fail": "Something goes wrong while revoking.", + "modal_prorevokeauth": "Revoke auth by clicking here and follow the steps.", "modal_prorevokeauth_clean": "Clean", "modal_prorevokeauth_clean_desc": "Clean local auth record", @@ -131,9 +147,29 @@ "settings_pcloud_connect_succ": "Great! We can connect to pCloud!", "settings_pcloud_connect_fail": "We cannot connect to pCloud.", + + "settings_yandexdisk": "Yandex Disk (PRO) (beta)", + "settings_chooseservice_yandexdisk": "Yandex Disk (PRO) (beta)", + "settings_yandexdisk_disclaimer1": "Disclaimer: This app is NOT an official Yandex Disk product. The app just uses Yandex Disk's public api.", + "settings_yandexdisk_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Yandex Disk, please immediately disconnect this app on https://app.yandexdisk.com/account/security .", + "settings_yandexdisk_pro_desc": "

!!It's a PRO feature of Remotely Save! You need a Remotely Save online account for this feature!!(scroll down for more info about PRO account.)

", + "settings_yandexdisk_notshowuphint": "Yandex Disk Settings Not Available", + "settings_yandexdisk_notshowuphint_desc": "Yandex Disk settings are not available, because you haven't subscribed to the PRO feature in your Remotely Save account.", + "settings_yandexdisk_notshowuphint_view_pro": "View PRO Settings", + "settings_yandexdisk_folder": "We will create and sync inside the folder {{remoteBaseDir}} on your Yandex Disk. DO NOT create this folder by yourself manually.", + "settings_yandexdisk_revoke": "Revoke Auth", + "settings_yandexdisk_revoke_desc": "You've connected. If you want to disconnect, click this button.", + "settings_yandexdisk_revoke_button": "Revoke Auth", + "settings_yandexdisk_auth": "Auth", + "settings_yandexdisk_auth_desc": "Auth.", + "settings_yandexdisk_auth_button": "Auth", + "settings_yandexdisk_connect_succ": "Great! We can connect to Yandex Disk!", + "settings_yandexdisk_connect_fail": "We cannot connect to Yandex Disk.", + "settings_export_googledrive_button": "Export Google Drive Part", "settings_export_box_button": "Export Box Part", "settings_export_pcloud_button": "Export pCloud Part", + "settings_export_yandexdisk_button": "Export Yandex Disk Part", "settings_pro": "Account (for PRO features)", "settings_pro_tutorial": "

Using basic features of Remotely Save is FREE and do NOT need an account.

However, you will need an online account and PAY for the PRO features such as smart conflict.

Firstly please click the button to sign up and sign in to the website: https://remotelysave.com. Notice: It's different from, and NOT affiliated with Obsidian account.

Secondly please \"connect\" your local device to your online account.", diff --git a/pro/src/langs/zh_cn.json b/pro/src/langs/zh_cn.json index 51b79f9..57c1d8a 100644 --- a/pro/src/langs/zh_cn.json +++ b/pro/src/langs/zh_cn.json @@ -17,6 +17,11 @@ "protocol_pcloud_connect_fail": "pCloud 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?", "protocol_pcloud_connect_succ_revoke": "您已连接上账号。如果要取消连接,请点击此按钮。", + "protocol_yandexdisk_connecting": "正在连接", + "protocol_yandexdisk_connect_manualinput_succ": "连接成功", + "protocol_yandexdisk_connect_fail": "Yandex Disk 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?", + "protocol_yandexdisk_connect_succ_revoke": "您已连接上账号。如果要取消连接,请点击此按钮。", + "modal_googledriveauth_tutorial": "

请访问此网址,然后会进入授权流程。最后,您会看到一个码,请复制粘贴到这里然后提交。

", "modal_googledriveauth_copybutton": "点击以复制网址", "modal_googledriveauth_copynotice": "网址已复制!", @@ -60,6 +65,22 @@ "modal_pcloudrevokeauth_clean_notice": "已清理!", "modal_pcloudrevokeauth_clean_fail": "清理授权时候发生了错误。", + "modal_yandexdiskauth_tutorial": "

请访问此网址,然后会进入授权流程。最后,您会被重定向回来。

", + "modal_yandexdiskauth_copybutton": "点击以复制网址", + "modal_yandexdiskauth_copynotice": "网址已复制!", + "modal_yandexdisk_maualinput": "网站上的码", + "modal_yandexdisk_maualinput_desc": "请粘贴授权流程最后的那个码,然后点击确认。", + "modal_yandexdisk_maualinput_notice": "正在尝试连接 Yandex Disk 并更新授权信息......", + "modal_yandexdisk_maualinput_succ_notice": "很好!授权信息已更新!", + "modal_yandexdisk_maualinput_fail_notice": "更新授权信息失败。请稍后重试。", + "modal_yandexdiskrevokeauth_step1": "第 1 步:访问以下网址,可以删除连接。", + "modal_yandexdiskrevokeauth_step2": "第 2 步:点击以下按钮,从而清理本地的登录信息。", + "modal_yandexdiskrevokeauth_clean": "清理本地登录信息", + "modal_yandexdiskrevokeauth_clean_desc": "您需要点击此按钮。", + "modal_yandexdiskrevokeauth_clean_button": "清理", + "modal_yandexdiskrevokeauth_clean_notice": "已清理!", + "modal_yandexdiskrevokeauth_clean_fail": "清理授权时候发生了错误。", + "modal_prorevokeauth": "点击这里和按照步骤取消授权。", "modal_prorevokeauth_clean": "清理", "modal_prorevokeauth_clean_desc": "清理本地授权记录", @@ -131,9 +152,28 @@ "settings_pcloud_connect_succ": "很好!我们可连接上 pCloud!", "settings_pcloud_connect_fail": "我们未能连接上 pCloud。", + "settings_yandexdisk": "Yandex Disk (PRO) (beta)", + "settings_chooseservice_yandexdisk": "Yandex Disk (PRO) (beta)", + "settings_yandexdisk_disclaimer1": "声明:本插件不是 Yandex Disk 的官方产品。只是用到了它的公开 API。", + "settings_yandexdisk_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现任何不符合预期的 Yandex Disk 访问,请立刻在以下网站操作断开连接: https://id.yandex.com/profile/devices 。", + "settings_yandexdisk_pro_desc": "

!!这是 PRO(付费)功能! 您需要在线账号来使用此功能!!向下滑可以看到 PRO 账号的更多信息。)

", + "settings_yandexdisk_notshowuphint": "Yandex Disk 设置不可用", + "settings_yandexdisk_notshowuphint_desc": "Yandex Disk 设置不可用,因为您没有在 Remotely Save 账号里开启这个 PRO 功能。", + "settings_yandexdisk_notshowuphint_view_pro": "查看 PRO 相关设置", + "settings_yandexdisk_folder": "我们会在 Yandex Disk 创建此文件夹并同步内容进去: {{remoteBaseDir}} 。请不要手动在网站上创建。", + "settings_yandexdisk_revoke": "撤回鉴权", + "settings_yandexdisk_revoke_desc": "您现在已连接。如果想取消连接,请点击此按钮。", + "settings_yandexdisk_revoke_button": "撤回鉴权", + "settings_yandexdisk_auth": "鉴权", + "settings_yandexdisk_auth_desc": "鉴权.", + "settings_yandexdisk_auth_button": "鉴权", + "settings_yandexdisk_connect_succ": "很好!我们可连接上 Yandex Disk!", + "settings_yandexdisk_connect_fail": "我们未能连接上 Yandex Disk。", + "settings_export_googledrive_button": "导出 Google Drive 部分", "settings_export_box_button": "导出 Box 部分", "settings_export_pcloud_button": "导出 pCloud 部分", + "settings_export_yandexdisk_button": "导出 Yandex Disk 部分", "settings_pro": "账号(PRO 付费功能)", "settings_pro_tutorial": "

使用 Remotely Save 的基本功能是免费的,而且需要注册对应账号。

但是,您需要注册账号和对PRO功能付费使用,如智能处理冲突功能。

第一步:点击按钮从而注册和登录网站:https://remotelysave.com。注意:这和 Obsidian 官方账号无关,是不同的账号。

第二部:点击“连接”按钮,从而连接本设备和在线账号。", diff --git a/pro/src/langs/zh_tw.json b/pro/src/langs/zh_tw.json index ac0888f..ed86616 100644 --- a/pro/src/langs/zh_tw.json +++ b/pro/src/langs/zh_tw.json @@ -17,6 +17,11 @@ "protocol_pcloud_connect_fail": "pCloud 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?", "protocol_pcloud_connect_succ_revoke": "您已連線上賬號。如果要取消連線,請點選此按鈕。", + "protocol_yandexdisk_connecting": "正在連線", + "protocol_yandexdisk_connect_manualinput_succ": "連線成功", + "protocol_yandexdisk_connect_fail": "Yandex Disk 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?", + "protocol_yandexdisk_connect_succ_revoke": "您已連線上賬號。如果要取消連線,請點選此按鈕。", + "modal_googledriveauth_tutorial": "

請訪問此網址,然後會進入授權流程。最後,您會看到一個碼,請複製貼上到這裡然後提交。

", "modal_googledriveauth_copybutton": "點選以複製網址", "modal_googledriveauth_copynotice": "網址已複製!", @@ -60,6 +65,22 @@ "modal_pcloudrevokeauth_clean_notice": "已清理!", "modal_pcloudrevokeauth_clean_fail": "清理授權時候發生了錯誤。", + "modal_yandexdiskauth_tutorial": "

請訪問此網址,然後會進入授權流程。最後,您會被重定向回來。

", + "modal_yandexdiskauth_copybutton": "點選以複製網址", + "modal_yandexdiskauth_copynotice": "網址已複製!", + "modal_yandexdisk_maualinput": "網站上的碼", + "modal_yandexdisk_maualinput_desc": "請貼上授權流程最後的那個碼,然後點選確認。", + "modal_yandexdisk_maualinput_notice": "正在嘗試連線 Yandex Disk 並更新授權資訊......", + "modal_yandexdisk_maualinput_succ_notice": "很好!授權資訊已更新!", + "modal_yandexdisk_maualinput_fail_notice": "更新授權資訊失敗。請稍後重試。", + "modal_yandexdiskrevokeauth_step1": "第 1 步:訪問以下網址,可以刪除連線。", + "modal_yandexdiskrevokeauth_step2": "第 2 步:點選以下按鈕,從而清理本地的登入資訊。", + "modal_yandexdiskrevokeauth_clean": "清理本地登入資訊", + "modal_yandexdiskrevokeauth_clean_desc": "您需要點選此按鈕。", + "modal_yandexdiskrevokeauth_clean_button": "清理", + "modal_yandexdiskrevokeauth_clean_notice": "已清理!", + "modal_yandexdiskrevokeauth_clean_fail": "清理授權時候發生了錯誤。", + "modal_prorevokeauth": "點選這裡和按照步驟取消授權。", "modal_prorevokeauth_clean": "清理", "modal_prorevokeauth_clean_desc": "清理本地授權記錄", @@ -131,9 +152,28 @@ "settings_pcloud_connect_succ": "很好!我們可連線上 pCloud!", "settings_pcloud_connect_fail": "我們未能連線上 pCloud。", + "settings_yandexdisk": "Yandex Disk (PRO) (beta)", + "settings_chooseservice_yandexdisk": "Yandex Disk (PRO) (beta)", + "settings_yandexdisk_disclaimer1": "宣告:本外掛不是 Yandex Disk 的官方產品。只是用到了它的公開 API。", + "settings_yandexdisk_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現任何不符合預期的 Yandex Disk 訪問,請立刻在以下網站操作斷開連線: https://id.yandex.com/profile/devices 。", + "settings_yandexdisk_pro_desc": "

!!這是 PRO(付費)功能! 您需要線上賬號來使用此功能!!向下滑可以看到 PRO 賬號的更多資訊。)

", + "settings_yandexdisk_notshowuphint": "Yandex Disk 設定不可用", + "settings_yandexdisk_notshowuphint_desc": "Yandex Disk 設定不可用,因為您沒有在 Remotely Save 賬號裡開啟這個 PRO 功能。", + "settings_yandexdisk_notshowuphint_view_pro": "檢視 PRO 相關設定", + "settings_yandexdisk_folder": "我們會在 Yandex Disk 建立此資料夾並同步內容進去: {{remoteBaseDir}} 。請不要手動在網站上建立。", + "settings_yandexdisk_revoke": "撤回鑑權", + "settings_yandexdisk_revoke_desc": "您現在已連線。如果想取消連線,請點選此按鈕。", + "settings_yandexdisk_revoke_button": "撤回鑑權", + "settings_yandexdisk_auth": "鑑權", + "settings_yandexdisk_auth_desc": "鑑權.", + "settings_yandexdisk_auth_button": "鑑權", + "settings_yandexdisk_connect_succ": "很好!我們可連線上 Yandex Disk!", + "settings_yandexdisk_connect_fail": "我們未能連線上 Yandex Disk。", + "settings_export_googledrive_button": "匯出 Google Drive 部分", "settings_export_box_button": "匯出 Box 部分", "settings_export_pcloud_button": "匯出 pCloud 部分", + "settings_export_yandexdisk_button": "匯出 Yandex Disk 部分", "settings_pro": "賬號(PRO 付費功能)", "settings_pro_tutorial": "

使用 Remotely Save 的基本功能是免費的,而且需要註冊對應賬號。

但是,您需要註冊賬號和對PRO功能付費使用,如智慧處理衝突功能。

第一步:點選按鈕從而註冊和登入網站:https://remotelysave.com。注意:這和 Obsidian 官方賬號無關,是不同的賬號。

第二部:點選“連線”按鈕,從而連線本裝置和線上賬號。", diff --git a/pro/src/settingsPro.ts b/pro/src/settingsPro.ts index 5ca9e21..794f3cd 100644 --- a/pro/src/settingsPro.ts +++ b/pro/src/settingsPro.ts @@ -253,7 +253,9 @@ export const generateProSettingsPart = ( boxAllowedToUsedDiv: HTMLDivElement, boxNotShowUpHintSetting: Setting, pCloudAllowedToUsedDiv: HTMLDivElement, - pCloudNotShowUpHintSetting: Setting + pCloudNotShowUpHintSetting: Setting, + yandexDiskAllowedToUsedDiv: HTMLDivElement, + yandexDiskNotShowUpHintSetting: Setting ) => { proDiv .createEl("h2", { text: t("settings_pro") }) @@ -351,6 +353,25 @@ export const generateProSettingsPart = ( ); } + const allowYandexDisk = + plugin.settings.pro?.enabledProFeatures.filter( + (x) => x.featureName === "feature-yandex_disk" + ).length === 1; + console.debug( + `allow to show up Yandex Disk settings? ${allowYandexDisk}` + ); + if (allowYandexDisk) { + yandexDiskAllowedToUsedDiv.removeClass("yandexdisk-allow-to-use-hide"); + yandexDiskNotShowUpHintSetting.settingEl.addClass( + "yandexdisk-allow-to-use-hide" + ); + } else { + yandexDiskAllowedToUsedDiv.addClass("yandexdisk-allow-to-use-hide"); + yandexDiskNotShowUpHintSetting.settingEl.removeClass( + "yandexdisk-allow-to-use-hide" + ); + } + new Notice(t("settings_pro_features_refresh_succ")); }); }); diff --git a/pro/src/settingsYandexDisk.ts b/pro/src/settingsYandexDisk.ts new file mode 100644 index 0000000..c22a4cf --- /dev/null +++ b/pro/src/settingsYandexDisk.ts @@ -0,0 +1,378 @@ +import cloneDeep from "lodash/cloneDeep"; +import { type App, Modal, Notice, Setting } from "obsidian"; +import { getClient } from "../../src/fsGetter"; +import type { TransItemType } from "../../src/i18n"; +import type RemotelySavePlugin from "../../src/main"; +import { stringToFragment } from "../../src/misc"; +import { ChangeRemoteBaseDirModal } from "../../src/settings"; +import { + DEFAULT_YANDEXDISK_CONFIG, + generateAuthUrl, + sendRefreshTokenReq, +} from "./fsYandexDisk"; + +class YandexDiskAuthModal extends Modal { + readonly plugin: RemotelySavePlugin; + readonly authDiv: HTMLDivElement; + readonly revokeAuthDiv: HTMLDivElement; + readonly revokeAuthSetting: Setting; + readonly t: (x: TransItemType, vars?: any) => string; + constructor( + app: App, + plugin: RemotelySavePlugin, + authDiv: HTMLDivElement, + revokeAuthDiv: HTMLDivElement, + revokeAuthSetting: Setting, + t: (x: TransItemType, vars?: any) => string + ) { + super(app); + this.plugin = plugin; + this.authDiv = authDiv; + this.revokeAuthDiv = revokeAuthDiv; + this.revokeAuthSetting = revokeAuthSetting; + this.t = t; + } + + async onOpen() { + const { contentEl } = this; + const t = this.t; + + const authUrl = generateAuthUrl(true); + const div2 = contentEl.createDiv(); + div2.createDiv({ + text: stringToFragment(t("modal_yandexdiskauth_tutorial")), + }); + div2.createEl( + "button", + { + text: t("modal_yandexdiskauth_copybutton"), + }, + (el) => { + el.onclick = async () => { + await navigator.clipboard.writeText(authUrl); + new Notice(t("modal_yandexdiskauth_copynotice")); + }; + } + ); + + contentEl.createEl("p").createEl("a", { + href: authUrl, + text: authUrl, + }); + + // let refreshToken = ""; + // new Setting(contentEl) + // .setName(t("modal_yandexdisk_maualinput")) + // .setDesc(t("modal_yandexdisk_maualinput_desc")) + // .addText((text) => + // text + // .setPlaceholder("") + // .setValue("") + // .onChange((val) => { + // refreshToken = val.trim(); + // }) + // ) + // .addButton(async (button) => { + // button.setButtonText(t("submit")); + // button.onClick(async () => { + // new Notice(t("modal_yandexdisk_maualinput_notice")); + + // try { + // if (this.plugin.settings.yandexdisk === undefined) { + // this.plugin.settings.yandexdisk = cloneDeep( + // DEFAULT_YANDEXDISK_CONFIG + // ); + // } + // this.plugin.settings.yandexdisk.refreshToken = refreshToken; + // this.plugin.settings.yandexdisk.accessToken = "access"; + // this.plugin.settings.yandexdisk.accessTokenExpiresAtTimeMs = 1; + // this.plugin.settings.yandexdisk.accessTokenExpiresInMs = 1; + + // // TODO: abstraction leaking now, how to fix? + // const k = await sendRefreshTokenReq(refreshToken); + // const ts = Date.now(); + // this.plugin.settings.yandexdisk.accessToken = k.access_token; + // this.plugin.settings.yandexdisk.accessTokenExpiresInMs = + // k.expires_in * 1000; + // this.plugin.settings.yandexdisk.accessTokenExpiresAtTimeMs = + // ts + k.expires_in * 1000 - 60 * 2 * 1000; + // await this.plugin.saveSettings(); + + // // try to remove data in clipboard + // await navigator.clipboard.writeText(""); + + // new Notice(t("modal_yandexdisk_maualinput_succ_notice")); + // } catch (e) { + // console.error(e); + // new Notice(t("modal_yandexdisk_maualinput_fail_notice")); + // } finally { + // this.authDiv.toggleClass( + // "yandexdisk-auth-button-hide", + // this.plugin.settings.yandexdisk.refreshToken !== "" + // ); + // this.revokeAuthDiv.toggleClass( + // "yandexdisk-revoke-auth-button-hide", + // this.plugin.settings.yandexdisk.refreshToken === "" + // ); + // this.close(); + // } + // }); + // }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} + +class YandexDiskRevokeAuthModal extends Modal { + readonly plugin: RemotelySavePlugin; + readonly authDiv: HTMLDivElement; + readonly revokeAuthDiv: HTMLDivElement; + readonly t: (x: TransItemType, vars?: any) => string; + constructor( + app: App, + plugin: RemotelySavePlugin, + authDiv: HTMLDivElement, + revokeAuthDiv: HTMLDivElement, + t: (x: TransItemType, vars?: any) => string + ) { + super(app); + this.plugin = plugin; + this.authDiv = authDiv; + this.revokeAuthDiv = revokeAuthDiv; + this.t = t; + } + + async onOpen() { + const t = this.t; + const { contentEl } = this; + + contentEl.createEl("p", { + text: t("modal_yandexdiskrevokeauth_step1"), + }); + const consentUrl = "https://app.yandexDisk.com/account/security"; + contentEl.createEl("p").createEl("a", { + href: consentUrl, + text: consentUrl, + }); + + contentEl.createEl("p", { + text: t("modal_yandexdiskrevokeauth_step2"), + }); + + new Setting(contentEl) + .setName(t("modal_yandexdiskrevokeauth_clean")) + .setDesc(t("modal_yandexdiskrevokeauth_clean_desc")) + .addButton(async (button) => { + button.setButtonText(t("modal_yandexdiskrevokeauth_clean_button")); + button.onClick(async () => { + try { + this.plugin.settings.yandexdisk = cloneDeep( + DEFAULT_YANDEXDISK_CONFIG + ); + + await this.plugin.saveSettings(); + this.authDiv.toggleClass( + "yandexdisk-auth-button-hide", + this.plugin.settings.yandexdisk.refreshToken !== "" + ); + this.revokeAuthDiv.toggleClass( + "yandexdisk-revoke-auth-button-hide", + this.plugin.settings.yandexdisk.refreshToken === "" + ); + new Notice(t("modal_yandexdiskrevokeauth_clean_notice")); + this.close(); + } catch (err) { + console.error(err); + new Notice(t("modal_yandexdiskrevokeauth_clean_fail")); + } + }); + }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} + +export const generateYandexDiskSettingsPart = ( + containerEl: HTMLElement, + t: (x: TransItemType, vars?: any) => string, + app: App, + plugin: RemotelySavePlugin, + saveUpdatedConfigFunc: () => Promise | undefined +) => { + const yandexDiskDiv = containerEl.createEl("div", { + cls: "yandexdisk-hide", + }); + yandexDiskDiv.toggleClass( + "yandexdisk-hide", + plugin.settings.serviceType !== "yandexdisk" + ); + yandexDiskDiv.createEl("h2", { text: t("settings_yandexdisk") }); + + const yandexDiskLongDescDiv = yandexDiskDiv.createEl("div", { + cls: "settings-long-desc", + }); + for (const c of [ + t("settings_yandexdisk_disclaimer1"), + t("settings_yandexdisk_disclaimer2"), + ]) { + yandexDiskLongDescDiv.createEl("p", { + text: c, + cls: "yandexdisk-disclaimer", + }); + } + + yandexDiskLongDescDiv.createEl("p", { + text: t("settings_yandexdisk_folder", { + remoteBaseDir: + plugin.settings.yandexdisk.remoteBaseDir || app.vault.getName(), + }), + }); + + yandexDiskLongDescDiv.createDiv({ + text: stringToFragment(t("settings_yandexdisk_pro_desc")), + cls: "yandexdisk-disclaimer", + }); + + const yandexDiskNotShowUpHintSetting = new Setting(yandexDiskDiv) + .setName(t("settings_yandexdisk_notshowuphint")) + .setDesc(t("settings_yandexdisk_notshowuphint_desc")) + .addButton(async (button) => { + button.setButtonText(t("settings_yandexdisk_notshowuphint_view_pro")); + button.onClick(async () => { + window.location.href = "#settings-pro"; + }); + }); + + const yandexDiskAllowedToUsedDiv = yandexDiskDiv.createDiv(); + // if pro enabled, show up; otherwise hide. + const allowYandexDisk = + plugin.settings.pro?.enabledProFeatures.filter( + (x) => x.featureName === "feature-yandex_disk" + ).length === 1; + console.debug(`allow to show up yandexDisk settings? ${allowYandexDisk}`); + if (allowYandexDisk) { + yandexDiskAllowedToUsedDiv.removeClass("yandexdisk-allow-to-use-hide"); + yandexDiskNotShowUpHintSetting.settingEl.addClass( + "yandexdisk-allow-to-use-hide" + ); + } else { + yandexDiskAllowedToUsedDiv.addClass("yandexdisk-allow-to-use-hide"); + yandexDiskNotShowUpHintSetting.settingEl.removeClass( + "yandexdisk-allow-to-use-hide" + ); + } + + const yandexDiskSelectAuthDiv = yandexDiskAllowedToUsedDiv.createDiv(); + const yandexDiskAuthDiv = yandexDiskSelectAuthDiv.createDiv({ + cls: "yandexdisk-auth-button-hide settings-auth-related", + }); + const yandexDiskRevokeAuthDiv = yandexDiskSelectAuthDiv.createDiv({ + cls: "yandexdisk-revoke-auth-button-hide settings-auth-related", + }); + + const yandexDiskRevokeAuthSetting = new Setting(yandexDiskRevokeAuthDiv) + .setName(t("settings_yandexdisk_revoke")) + .setDesc(t("settings_yandexdisk_revoke_desc")) + .addButton(async (button) => { + button.setButtonText(t("settings_yandexdisk_revoke_button")); + button.onClick(async () => { + new YandexDiskRevokeAuthModal( + app, + plugin, + yandexDiskAuthDiv, + yandexDiskRevokeAuthDiv, + t + ).open(); + }); + }); + + new Setting(yandexDiskAuthDiv) + .setName(t("settings_yandexdisk_auth")) + .setDesc(t("settings_yandexdisk_auth_desc")) + .addButton(async (button) => { + button.setButtonText(t("settings_yandexdisk_auth_button")); + button.onClick(async () => { + const modal = new YandexDiskAuthModal( + app, + plugin, + yandexDiskAuthDiv, + yandexDiskRevokeAuthDiv, + yandexDiskRevokeAuthSetting, + t + ); + plugin.oauth2Info.helperModal = modal; + plugin.oauth2Info.authDiv = yandexDiskAuthDiv; + plugin.oauth2Info.revokeDiv = yandexDiskRevokeAuthDiv; + plugin.oauth2Info.revokeAuthSetting = yandexDiskRevokeAuthSetting; + modal.open(); + }); + }); + + yandexDiskAuthDiv.toggleClass( + "yandexdisk-auth-button-hide", + plugin.settings.yandexdisk.refreshToken !== "" + ); + yandexDiskRevokeAuthDiv.toggleClass( + "yandexdisk-revoke-auth-button-hide", + plugin.settings.yandexdisk.refreshToken === "" + ); + + let newyandexDiskRemoteBaseDir = + plugin.settings.yandexdisk.remoteBaseDir || ""; + new Setting(yandexDiskAllowedToUsedDiv) + .setName(t("settings_remotebasedir")) + .setDesc(t("settings_remotebasedir_desc")) + .addText((text) => + text + .setPlaceholder(app.vault.getName()) + .setValue(newyandexDiskRemoteBaseDir) + .onChange((value) => { + newyandexDiskRemoteBaseDir = value.trim(); + }) + ) + .addButton((button) => { + button.setButtonText(t("confirm")); + button.onClick(() => { + new ChangeRemoteBaseDirModal( + app, + plugin, + newyandexDiskRemoteBaseDir, + "yandexdisk" + ).open(); + }); + }); + new Setting(yandexDiskAllowedToUsedDiv) + .setName(t("settings_checkonnectivity")) + .setDesc(t("settings_checkonnectivity_desc")) + .addButton(async (button) => { + button.setButtonText(t("settings_checkonnectivity_button")); + button.onClick(async () => { + new Notice(t("settings_checkonnectivity_checking")); + const client = getClient(plugin.settings, app.vault.getName(), () => + plugin.saveSettings() + ); + const errors = { msg: "" }; + const res = await client.checkConnect((err: any) => { + errors.msg = `${err}`; + }); + if (res) { + new Notice(t("settings_yandexdisk_connect_succ")); + } else { + new Notice(t("settings_yandexdisk_connect_fail")); + new Notice(errors.msg); + } + }); + }); + + return { + yandexDiskDiv: yandexDiskDiv, + yandexDiskAllowedToUsedDiv: yandexDiskAllowedToUsedDiv, + yandexDiskNotShowUpHintSetting: yandexDiskNotShowUpHintSetting, + }; +}; diff --git a/pro/src/yandexApi.ts b/pro/src/yandexApi.ts new file mode 100644 index 0000000..e897529 --- /dev/null +++ b/pro/src/yandexApi.ts @@ -0,0 +1,307 @@ +////////////////////////////////////////////// +// all types +// https://yandex.com/dev/disk-api/doc/en/reference/response-objects +////////////////////////////////////////////// + +export interface Link { + href: string; + method: string; + templated: boolean; +} + +export interface Resource { + antivirus_status?: string; + public_key?: string; + _embedded?: ResourceList; + name?: string; + created?: string; + custom_properties?: Record; + public_url?: string; + origin_path?: string; + modified?: string; + path?: string; + md5?: string; + sha256?: string; + file?: string; + type?: "dir" | "file"; + mime_type?: string; + size?: number; + exif?: Record; +} + +type SortType = string; + +export interface ResourceList { + sort?: SortType; + public_key?: string; + items?: Resource[]; + path?: string; + limit?: number; + offset?: number; + total?: number; +} + +export interface FilesResourceList { + items: Resource[]; + limit: number; + offset: number; +} + +export interface LastUploadedResourceList { + items: Resource[]; + limit: number; +} + +export interface PublicResourcesList { + items: Resource[]; + type: "dir" | "file"; + limit: number; + offset: number; +} + +export interface Disk { + trash_size: number; + total_space: number; + used_space: number; + system_folders: { + applications: string; + downloads: string; + }; +} + +export interface Operation { + status: "success" | "failed" | "in-progress"; +} + +export interface ErrorResponse { + error: string; + description: string; + message?: string; +} + +////////////////////////////////////////////// +// api +// https://yandex.com/dev/disk-api/doc/en/ +////////////////////////////////////////////// + +export class YandexApi { + accessToken: string; + apiVersion: number; + host: string; + constructor(accessToken: string) { + this.accessToken = accessToken; + this.apiVersion = 1; + this.host = `https://cloud-api.yandex.net`; + } + + async _api( + method: string, + endPoint: string, + returnType: "json" | "arrayBuffer" | "raw", + queryParams?: Record, + body?: any, + requestContentType?: string + ) { + let p = ""; + if (queryParams !== undefined) { + p = `?${new URLSearchParams(queryParams)}`; + } + + const fullUrl = `${this.host}/v${this.apiVersion}/${endPoint}${p}`; + // console.debug(`method: ${method}, fullUrl: ${fullUrl}`) + const headers: HeadersInit = { + Authorization: `OAuth ${this.accessToken}`, + }; + if (requestContentType !== undefined) { + headers["Content-Type"] = requestContentType; + } + const r = await fetch(fullUrl, { + method: method, + headers: headers, + body: body, + }); + // console.debug(r) + + if (r.status >= 200 && r.status < 300) { + if (returnType === "json") { + return await r.json(); + } + if (returnType === "arrayBuffer") { + return await r.arrayBuffer(); + } + if (returnType === "raw") { + return r; + } + } else { + throw Error(JSON.stringify((await r.json()) as ErrorResponse)); + } + } + + async disk() { + return (await this._api("GET", "disk/", "json")) as Disk; + } + + async diskResources( + path: string, + fields?: string[], + limit?: number, + offset?: number + ) { + const params: Record = { + path: path, + }; + if (fields !== undefined) { + params["fields"] = fields.join(","); + } + if (limit !== undefined) { + params["limit"] = `${limit}`; + } + if (offset !== undefined) { + params["offset"] = `${offset}`; + } + + return (await this._api( + "GET", + "disk/resources", + "json", + params + )) as Resource; + } + + async diskResourcesFiles(fields?: string[], limit?: number, offset?: number) { + const params: Record = {}; + if (fields !== undefined) { + params["fields"] = fields.join(","); + } + if (limit !== undefined) { + params["limit"] = `${limit}`; + } + if (offset !== undefined) { + params["offset"] = `${offset}`; + } + return (await this._api( + "GET", + "disk/resources/files", + "json", + params + )) as FilesResourceList; + } + + async diskResourcesPatch( + path: string, + custom_properties: Record, + fields?: string[] + ) { + const params: Record = { + path: path, + }; + if (fields !== undefined) { + params["fields"] = fields.join(","); + } + return (await this._api( + "PATCH", + "disk/resources", + "json", + params, + JSON.stringify({ + custom_properties: custom_properties, + }), + "application/json" + )) as Resource; + } + + async diskResoucesUpload( + path: string, + content: ArrayBuffer, + overwrite?: boolean + ) { + const params: Record = { + path: path, + overwrite: `${overwrite ?? false}`, + }; + const link = (await this._api( + "GET", + "disk/resources/upload", + "json", + params + )) as Link; + + if (link.templated) { + throw Error( + `do not know how to deal with upload link with templated: ${JSON.stringify( + link + )}` + ); + } + + const rsp = await fetch(link.href, { + method: link.method, + body: content, + }); + if (rsp.status === 200 || rsp.status === 201 || rsp.status === 202) { + return true; + } else { + throw Error(`upload failed. status=${rsp.status}, link=${link.href}`); + } + } + + async diskResoucesDownload(path: string) { + const params: Record = { + path: path, + }; + const link = (await this._api( + "GET", + "disk/resources/download", + "json", + params + )) as Link; + + if (link.templated) { + throw Error( + `do not know how to deal with download link with templated: ${JSON.stringify( + link + )}` + ); + } + + const rsp = await fetch(link.href, { + method: link.method, + }); + if (rsp.status === 200 || rsp.status === 201 || rsp.status === 202) { + return await rsp.arrayBuffer(); + } else { + throw Error(`download failed. status=${rsp.status}, link=${link.href}`); + } + } + + async diskResourcesDelete(path: string, permanently?: boolean) { + const params: Record = { + path: path, + permanently: `${permanently ?? false}`, + }; + const rsp = (await this._api( + "DELETE", + "disk/resources", + "raw", + params + )) as Response; + + if (rsp.status === 204) { + return; + } + if (rsp.status === 202) { + return (await rsp.json()) as Link; + } + throw Error(`do not know how to deal with delete response ${rsp}`); + } + + async diskResourcesPut(path: string, fields?: string[]) { + const params: Record = { + path: path, + }; + if (fields !== undefined) { + params["fields"] = fields.join(","); + } + return (await this._api("PUT", "disk/resources", "json", params)) as Link; + } +} diff --git a/src/baseTypes.ts b/src/baseTypes.ts index 2ce0988..1eb9d4c 100644 --- a/src/baseTypes.ts +++ b/src/baseTypes.ts @@ -8,6 +8,7 @@ import type { GoogleDriveConfig, PCloudConfig, ProConfig, + YandexDiskConfig, } from "../pro/src/baseTypesPro"; import type { LangTypeAndAuto } from "./i18n"; @@ -21,7 +22,8 @@ export type SUPPORTED_SERVICES_TYPE = | "webdis" | "googledrive" | "box" - | "pcloud"; + | "pcloud" + | "yandexdisk"; export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR = | "webdav" @@ -30,7 +32,8 @@ export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR = | "webdis" | "googledrive" | "box" - | "pcloud"; + | "pcloud" + | "yandexdisk"; export interface S3Config { s3Endpoint: string; @@ -127,7 +130,8 @@ export type QRExportType = | "webdis" | "googledrive" | "box" - | "pcloud"; + | "pcloud" + | "yandexdisk"; export interface ProfilerConfig { enablePrinting?: boolean; @@ -143,6 +147,7 @@ export interface RemotelySavePluginSettings { googledrive: GoogleDriveConfig; box: BoxConfig; pcloud: PCloudConfig; + yandexdisk: YandexDiskConfig; password: string; serviceType: SUPPORTED_SERVICES_TYPE; currLogLevel?: string; diff --git a/src/fsGetter.ts b/src/fsGetter.ts index 10f7790..6c17ba5 100644 --- a/src/fsGetter.ts +++ b/src/fsGetter.ts @@ -1,6 +1,7 @@ import { FakeFsBox } from "../pro/src/fsBox"; import { FakeFsGoogleDrive } from "../pro/src/fsGoogleDrive"; import { FakeFsPCloud } from "../pro/src/fsPCloud"; +import { FakeFsYandexDisk } from "../pro/src/fsYandexDisk"; import type { RemotelySavePluginSettings } from "./baseTypes"; import type { FakeFs } from "./fsAll"; import { FakeFsDropbox } from "./fsDropbox"; @@ -58,6 +59,12 @@ export function getClient( vaultName, saveUpdatedConfigFunc ); + case "yandexdisk": + return new FakeFsYandexDisk( + settings.yandexdisk, + vaultName, + saveUpdatedConfigFunc + ); default: throw Error(`cannot init client for serviceType=${settings.serviceType}`); } diff --git a/src/importExport.ts b/src/importExport.ts index 89f31e7..ff123b8 100644 --- a/src/importExport.ts +++ b/src/importExport.ts @@ -27,6 +27,7 @@ export const exportQrCodeUri = async ( delete settings2.googledrive; delete settings2.box; delete settings2.pcloud; + delete settings2.yandexdisk; delete settings2.pro; } else if (exportFields === "s3") { settings2 = { s3: cloneDeep(settings.s3) }; @@ -44,6 +45,8 @@ export const exportQrCodeUri = async ( settings2 = { box: cloneDeep(settings.box) }; } else if (exportFields === "pcloud") { settings2 = { pcloud: cloneDeep(settings.pcloud) }; + } else if (exportFields === "yandexdisk") { + settings2 = { yandexdisk: cloneDeep(settings.yandexdisk) }; } delete settings2.vaultRandomID; diff --git a/src/main.ts b/src/main.ts index f2e42c3..2658c0c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,6 +27,7 @@ import { COMMAND_CALLBACK_BOX, COMMAND_CALLBACK_PCLOUD, COMMAND_CALLBACK_PRO, + COMMAND_CALLBACK_YANDEXDISK, } from "../pro/src/baseTypesPro"; import { DEFAULT_BOX_CONFIG, @@ -42,6 +43,11 @@ import { sendAuthReq as sendAuthReqPCloud, setConfigBySuccessfullAuthInplace as setConfigBySuccessfullAuthInplacePCloud, } from "../pro/src/fsPCloud"; +import { + DEFAULT_YANDEXDISK_CONFIG, + sendAuthReq as sendAuthReqYandexDisk, + setConfigBySuccessfullAuthInplace as setConfigBySuccessfullAuthInplaceYandexDisk, +} from "../pro/src/fsYandexDisk"; import type { RemotelySavePluginSettings, SyncTriggerSourceType, @@ -99,6 +105,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = { googledrive: DEFAULT_GOOGLEDRIVE_CONFIG, box: DEFAULT_BOX_CONFIG, pcloud: DEFAULT_PCLOUD_CONFIG, + yandexdisk: DEFAULT_YANDEXDISK_CONFIG, password: "", serviceType: "s3", currLogLevel: "info", @@ -914,6 +921,62 @@ export default class RemotelySavePlugin extends Plugin { } ); + this.registerObsidianProtocolHandler( + COMMAND_CALLBACK_YANDEXDISK, + async (inputParams) => { + if (this.oauth2Info.helperModal !== undefined) { + const k = this.oauth2Info.helperModal.contentEl; + k.empty(); + + t("protocol_yandexdisk_connecting") + .split("\n") + .forEach((val) => { + k.createEl("p", { + text: val, + }); + }); + } + + console.debug(inputParams); + const authRes = await sendAuthReqYandexDisk( + inputParams.code, + async (e: any) => { + new Notice(t("protocol_yandexdisk_connect_fail")); + new Notice(`${e}`); + throw e; + } + ); + console.debug(authRes); + + const self = this; + await setConfigBySuccessfullAuthInplaceYandexDisk( + this.settings.yandexdisk!, + authRes, + () => self.saveSettings() + ); + + this.oauth2Info.verifier = ""; // reset it + this.oauth2Info.helperModal?.close(); // close it + this.oauth2Info.helperModal = undefined; + + this.oauth2Info.authDiv?.toggleClass( + "yandexdisk-auth-button-hide", + this.settings.yandexdisk?.refreshToken !== "" + ); + this.oauth2Info.authDiv = undefined; + + this.oauth2Info.revokeAuthSetting?.setDesc( + t("protocol_yandexdisk_connect_succ_revoke") + ); + this.oauth2Info.revokeAuthSetting = undefined; + this.oauth2Info.revokeDiv?.toggleClass( + "yandexdisk-revoke-auth-button-hide", + this.settings.yandexdisk?.refreshToken === "" + ); + this.oauth2Info.revokeDiv = undefined; + } + ); + this.syncRibbon = this.addRibbonIcon( iconNameSyncWait, `${this.manifest.name}`, @@ -1208,6 +1271,10 @@ export default class RemotelySavePlugin extends Plugin { this.settings.pcloud = DEFAULT_PCLOUD_CONFIG; } + if (this.settings.yandexdisk === undefined) { + this.settings.yandexdisk = DEFAULT_YANDEXDISK_CONFIG; + } + await this.saveSettings(); } @@ -1306,6 +1373,16 @@ export default class RemotelySavePlugin extends Plugin { needSave = true; } + let yandexDiskExpired = false; + if ( + this.settings.yandexdisk.refreshToken !== "" && + current >= this.settings!.yandexdisk!.credentialsShouldBeDeletedAtTimeMs! + ) { + yandexDiskExpired = true; + this.settings.yandexdisk = cloneDeep(DEFAULT_YANDEXDISK_CONFIG); + needSave = true; + } + if (this.settings.pro === undefined) { this.settings.pro = cloneDeep(DEFAULT_PRO_CONFIG); } @@ -1346,6 +1423,12 @@ export default class RemotelySavePlugin extends Plugin { 6000 ); } + if (yandexDiskExpired) { + new Notice( + `${this.manifest.name}: You haven't manually auth Yandex Disk for many days, you need to re-auth it again.`, + 6000 + ); + } } async getVaultRandomIDFromOldConfigFile() { diff --git a/src/settings.ts b/src/settings.ts index 2d7667d..cdf58cb 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -25,6 +25,7 @@ import { generateBoxSettingsPart } from "../pro/src/settingsBox"; import { generateGoogleDriveSettingsPart } from "../pro/src/settingsGoogleDrive"; import { generatePCloudSettingsPart } from "../pro/src/settingsPCloud"; import { generateProSettingsPart } from "../pro/src/settingsPro"; +import { generateYandexDiskSettingsPart } from "../pro/src/settingsYandexDisk"; import { API_VER_ENSURE_REQURL_OK, VALID_REQURL } from "./baseTypesObs"; import { messyConfigToNormal } from "./configPersist"; import { @@ -1820,7 +1821,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab { ); ////////////////////////////////////////////////// - // below for box + // below for pcloud ////////////////////////////////////////////////// const { pCloudDiv, pCloudAllowedToUsedDiv, pCloudNotShowUpHintSetting } = @@ -1828,6 +1829,22 @@ export class RemotelySaveSettingTab extends PluginSettingTab { this.plugin.saveSettings() ); + ////////////////////////////////////////////////// + // below for yandexdisk + ////////////////////////////////////////////////// + + const { + yandexDiskDiv, + yandexDiskAllowedToUsedDiv, + yandexDiskNotShowUpHintSetting, + } = generateYandexDiskSettingsPart( + containerEl, + t, + this.app, + this.plugin, + () => this.plugin.saveSettings() + ); + ////////////////////////////////////////////////// // below for general chooser (part 2/2) ////////////////////////////////////////////////// @@ -1849,6 +1866,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab { ); dropdown.addOption("box", t("settings_chooseservice_box")); dropdown.addOption("pcloud", t("settings_chooseservice_pcloud")); + dropdown.addOption( + "yandexdisk", + t("settings_chooseservice_yandexdisk") + ); dropdown .setValue(this.plugin.settings.serviceType) @@ -1886,6 +1907,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab { "pcloud-hide", this.plugin.settings.serviceType !== "pcloud" ); + yandexDiskDiv.toggleClass( + "yandexdisk-hide", + this.plugin.settings.serviceType !== "yandexdisk" + ); await this.plugin.saveSettings(); }); }); @@ -2460,6 +2485,16 @@ export class RemotelySaveSettingTab extends PluginSettingTab { button.onClick(async () => { new ExportSettingsQrCodeModal(this.app, this.plugin, "pcloud").open(); }); + }) + .addButton(async (button) => { + button.setButtonText(t("settings_export_yandexdisk_button")); + button.onClick(async () => { + new ExportSettingsQrCodeModal( + this.app, + this.plugin, + "yandexdisk" + ).open(); + }); }); let importSettingVal = ""; @@ -2530,7 +2565,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab { boxAllowedToUsedDiv, boxNotShowUpHintSetting, pCloudAllowedToUsedDiv, - pCloudNotShowUpHintSetting + pCloudNotShowUpHintSetting, + yandexDiskAllowedToUsedDiv, + yandexDiskNotShowUpHintSetting ); ////////////////////////////////////////////////// diff --git a/styles.css b/styles.css index e13b507..8c44828 100644 --- a/styles.css +++ b/styles.css @@ -129,6 +129,25 @@ display: none; } +.yandexdisk-disclaimer { + font-weight: bold; +} +.yandexdisk-hide { + display: none; +} + +.yandexdisk-allow-to-use-hide { + display: none; +} + +.yandexdisk-auth-button-hide { + display: none; +} + +.yandexdisk-revoke-auth-button-hide { + display: none; +} + .qrcode-img { width: 350px; height: 350px; diff --git a/tests/configPersist.test.ts b/tests/configPersist.test.ts index 7241c9e..77bee79 100644 --- a/tests/configPersist.test.ts +++ b/tests/configPersist.test.ts @@ -26,6 +26,9 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = { refreshToken: "xxx", } as any, pcloud: { + accessToken: "xxx", + } as any, + yandexdisk: { refreshToken: "xxx", } as any, password: "password", diff --git a/webpack.config.js b/webpack.config.js index 1fe5cb0..d4065df 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,6 +15,9 @@ const DEFAULT_BOX_CLIENT_ID = process.env.BOX_CLIENT_ID || ""; const DEFAULT_BOX_CLIENT_SECRET = process.env.BOX_CLIENT_SECRET || ""; const DEFAULT_PCLOUD_CLIENT_ID = process.env.PCLOUD_CLIENT_ID || ""; const DEFAULT_PCLOUD_CLIENT_SECRET = process.env.PCLOUD_CLIENT_SECRET || ""; +const DEFAULT_YANDEXDISK_CLIENT_ID = process.env.YANDEXDISK_CLIENT_ID || ""; +const DEFAULT_YANDEXDISK_CLIENT_SECRET = + process.env.YANDEXDISK_CLIENT_SECRET || ""; module.exports = { entry: "./src/main.ts", @@ -37,6 +40,8 @@ module.exports = { "process.env.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`, "process.env.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`, "process.env.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`, + "process.env.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`, + "process.env.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`, }), // Work around for Buffer is undefined: // https://github.com/webpack/changelog-v5/issues/10