add yandex disk pro

This commit is contained in:
fyears
2024-06-11 00:01:36 +08:00
parent 5186dcb967
commit 2e09686675
25 changed files with 1638 additions and 44 deletions
+33 -33
View File
@@ -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) {
+17
View File
@@ -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
+22 -1
View File
@@ -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";
}
-4
View File
@@ -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<Entity[]> {
+516
View File
@@ -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<any> | 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<any>;
constructor(
yandexDiskConfig: YandexDiskConfig,
vaultName: string,
saveUpdatedConfigFunc: () => Promise<any>
) {
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<Entity[]> {
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<Entity[]> {
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<Entity> {
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<Entity> {
// 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<string, string> = {};
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<Entity> {
// 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<string, string> = {};
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<ArrayBuffer> {
// 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<void> {
await this._init();
throw new Error("Method not implemented.");
}
async rm(key: string): Promise<void> {
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<boolean> {
// 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<string> {
await this._init();
throw new Error("Method not implemented.");
}
/**
* https://yandex.com/dev/id/doc/en/tokens/token-invalidate
*/
async revokeAuth(): Promise<any> {
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;
}
}
+36
View File
@@ -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": "<p>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.</p>",
"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": "<p>Please firstly go to the address, then go on the auth flow. In the end, you will be redirected to here.</p>",
"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": "<p><strong>!!It's a PRO feature of Remotely Save! You need a Remotely Save online account for this feature!!</strong>(<a href=\"#settings-pro\">scroll down</a> for more info about PRO account.)</p>",
"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": "<p>Using <stong>basic</strong> features of Remotely Save is <strong>FREE</strong> and do <strong>NOT</strong> need an account.</p><p>However, you will <strong>need</strong> an online account and <strong>PAY</strong> for the <strong>PRO</strong> features such as smart conflict.</p><p>Firstly please click the button to sign up and sign in to the website: <a href=\"https://remotelysave.com\">https://remotelysave.com</a>. Notice: It's different from, and NOT affiliated with Obsidian account.</p><p>Secondly please \"connect\" your local device to your online account.",
+40
View File
@@ -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": "<p>请访问此网址,然后会进入授权流程。最后,您会看到一个码,请复制粘贴到这里然后提交。</p>",
"modal_googledriveauth_copybutton": "点击以复制网址",
"modal_googledriveauth_copynotice": "网址已复制!",
@@ -60,6 +65,22 @@
"modal_pcloudrevokeauth_clean_notice": "已清理!",
"modal_pcloudrevokeauth_clean_fail": "清理授权时候发生了错误。",
"modal_yandexdiskauth_tutorial": "<p>请访问此网址,然后会进入授权流程。最后,您会被重定向回来。</p>",
"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": "<p><strong>!!这是 PRO(付费)功能! 您需要在线账号来使用此功能!!</strong><a href=\"#settings-pro\">向下滑</a>可以看到 PRO 账号的更多信息。)</p>",
"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": "<p>使用 Remotely Save 的<stong>基本</strong>功能是<strong>免费的</strong>,而且<strong>不</strong>需要注册对应账号。</p><p>但是,您<strong>需要</strong>注册账号和对<strong>PRO</strong>功能<strong>付费</strong>使用,如智能处理冲突功能。</p><p>第一步:点击按钮从而注册和登录网站:<a href=\"https://remotelysave.com\">https://remotelysave.com</a>。注意:这和 Obsidian 官方账号无关,是不同的账号。</p><p>第二部:点击“连接”按钮,从而连接本设备和在线账号。",
+40
View File
@@ -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": "<p>請訪問此網址,然後會進入授權流程。最後,您會看到一個碼,請複製貼上到這裡然後提交。</p>",
"modal_googledriveauth_copybutton": "點選以複製網址",
"modal_googledriveauth_copynotice": "網址已複製!",
@@ -60,6 +65,22 @@
"modal_pcloudrevokeauth_clean_notice": "已清理!",
"modal_pcloudrevokeauth_clean_fail": "清理授權時候發生了錯誤。",
"modal_yandexdiskauth_tutorial": "<p>請訪問此網址,然後會進入授權流程。最後,您會被重定向回來。</p>",
"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": "<p><strong>!!這是 PRO(付費)功能! 您需要線上賬號來使用此功能!!</strong><a href=\"#settings-pro\">向下滑</a>可以看到 PRO 賬號的更多資訊。)</p>",
"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": "<p>使用 Remotely Save 的<stong>基本</strong>功能是<strong>免費的</strong>,而且<strong>不</strong>需要註冊對應賬號。</p><p>但是,您<strong>需要</strong>註冊賬號和對<strong>PRO</strong>功能<strong>付費</strong>使用,如智慧處理衝突功能。</p><p>第一步:點選按鈕從而註冊和登入網站:<a href=\"https://remotelysave.com\">https://remotelysave.com</a>。注意:這和 Obsidian 官方賬號無關,是不同的賬號。</p><p>第二部:點選“連線”按鈕,從而連線本裝置和線上賬號。",
+22 -1
View File
@@ -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"));
});
});
+378
View File
@@ -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<any> | 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,
};
};
+307
View File
@@ -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<string, string>;
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<any, any>;
}
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<string, string>,
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<string, string> = {
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<string, string> = {};
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<string, string | null>,
fields?: string[]
) {
const params: Record<string, string> = {
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<string, string> = {
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<string, string> = {
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<string, string> = {
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<string, string> = {
path: path,
};
if (fields !== undefined) {
params["fields"] = fields.join(",");
}
return (await this._api("PUT", "disk/resources", "json", params)) as Link;
}
}