diff --git a/README.md b/README.md index 550ff08..d5d9efc 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find - pCloud (PRO feature) - Yandex Disk (PRO feature) - Koofr (PRO feature) + - Azure Blob Storage (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. @@ -147,6 +148,10 @@ PRO (paid) feature "sync with Yandex Disk" allows users to to sync with Yandex D PRO (paid) feature "sync with Koofr" allows users to to sync with Koofr (using its native API instead of webdav). Tutorials and limitations are documented [here](./docs/remote_services/koofr/README.md). +### Azure Blob Storage (PRO feature) + +PRO (paid) feature "sync with Azure Blob Storage" allows users to to sync with Azure Blob Storage. Tutorials and limitations are documented [here](./docs/remote_services/azureblobstorage/README.md). + ## Smart Conflict (PRO feature) Basic (free) version can detect conflicts, but users have to choose to keep newer version or larger version of the files. diff --git a/docs/remote_services/azureblobstorage/README.md b/docs/remote_services/azureblobstorage/README.md new file mode 100644 index 0000000..debfbd2 --- /dev/null +++ b/docs/remote_services/azureblobstorage/README.md @@ -0,0 +1,107 @@ +# Azure Blob Storage (GDrive) (PRO) + +# Intro + +* It's a PRO feature of Remotely Save plugin. +* **This plugin is NOT an official Microsoft / Azure product, and just uses Azure Blob Storage's public api.** + +# Disclaimer + +I (author of Remotely Save) is **NOT** an expert of Azure products. The tutorials here are for references only. Azure products are very complex. + +***As far as the law allows, the software (Remotely Save) comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** + +# Steps + +Please read through the following steps before you actually connect. + +## Preparation In Azure Blob Storage Side + +You only need to do this **once**, before the Container SAS Url expires in the future. + +In short, you need to: configure CORS and configure a policy and cobtain a Container SAS Url. + +1. Connect to the service: + + Download Microsoft's free [Azure Storage Explorer](https://azure.microsoft.com/en-us/products/storage/storage-explorer). The following tutorial uses this app. Actually you may be able to find the same settings on Azure official website. + + Use this app to connect to your Blob Storage service or account. + +2. CORS: + + Right click on `Blob containers`, click "configure CORS". Add a rule, enter the following and save: + ``` + Allowed Origins: * + Allowed Methods: DELETE,GET,HEAD,MERGE,POST,PATCH,OPTIONS,PUT + Allowed Headers: x-ms-*, content-type + Exposed Headers: x-ms-*, content-type + Max Age (in seconds): 5 + ``` + + ![CORS screenshot](./azure_cors.png) + +3. Create the container: + + Create the container if you don't have one. In this tutorial, we use `example-container`. + + ![container screenshot](./azure_example_container.png) + +4. Generate a policy: + + * Right click on you container, click "Manage Stored Access Policies". + * Choose your id, for example `example-container-0000001`. + * Change to Expiry time to an appropriate date. **By default it is only valid for a week.** After its expiration, you need come back and adjust the expiry date again! In this tutorial, we set it to a year. + * And allow these methods: Read, Add, Create, Write, Delete, List. + * Save + + Read Microsoft's official [doc](https://learn.microsoft.com/en-us/rest/api/storageservices/define-stored-access-policy) for more info. The main benefit of generating an access policy is easier revocation of SAS Url if anything goes bad. + + ![Manage Stored Access Policies screenshot](./azure_policy_1.png) + ![Add Policy screenshot](./azure_policy_3.png) + +5. Generate a Container SAS Url: + + Then we want to create a container level SAS (shared access signature) url. + + * Right click on you container, click "Get Shared Access Signature". + * In access policy, choose the previous one you created: for example `example-container-0000001`. + * Save the setting + * You will see a url is generated, which starts with `https://` or `http://`. It should looks like `https://.blob.core.windows.net/?sv=...&sig=...` + * Save the Container SAS Url somewhere, and you will need this later. + + Read Microsoft's official [doc](https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview) for more info. + + ![Container SAS Url screenshot 1](./azure_sas_1.png) + ![Container SAS Url screenshot 2](./azure_sas_2.png) + +### Why so complicated in Azure settings? + +Because Azure Blob Storage's api has some limitation in browser environment, so we need to configure CORS and SAS. Because we want to revoke the SAS when needed, we need to use a policy. + +### Revocation + +If you suspect someone read the resource unexpectedly, you can revoke the SAS by changing the name or expiry date of the policy. And generate the SAS Url again. + +## 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 Azure Blob Storage" 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 Azure Blob Storage should be detected. + +## Steps of Connecting to your Azure Blob Storage + +After you enabled the PRO feature in your Remotely Save plugin, **and prepared the Container SAS Url**, you can connect to your Azure Blob Storage account now. + +1. In Remotely Save settings, change your sync service to Azure Blob Storage. +2. Input your Container SAS Url +3. Input your container name. +4. By default, the plugin will save your vault remotely with a prefix `/`. You can change the prefix. No prefix is not allowed. +5. Check the connection. A notice will tell you that you've connected or not. +7. Sync! The plugin will upload the fils and "folders" of your vault into your Azure Blob Storage with the preix. +8. **Read the caveats below.** + +![RS setting screenshot](./azure_rs_setting.png) + +# The caveats + +* As of June 2024, this feature is in alpha stage. **Back up your vault before using this feature.** diff --git a/docs/remote_services/azureblobstorage/azure_cors.png b/docs/remote_services/azureblobstorage/azure_cors.png new file mode 100644 index 0000000..a1cb25f --- /dev/null +++ b/docs/remote_services/azureblobstorage/azure_cors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83caba9a42772f995035e3c879c6119eabcd6d3c27298cf0fb067b3676680571 +size 107057 diff --git a/docs/remote_services/azureblobstorage/azure_example_container.png b/docs/remote_services/azureblobstorage/azure_example_container.png new file mode 100644 index 0000000..feb5f8b --- /dev/null +++ b/docs/remote_services/azureblobstorage/azure_example_container.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef2d525d3ac7ecca26f7f8b6782fb1056d54f8c558a8d77586571defb7e6b5be +size 41282 diff --git a/docs/remote_services/azureblobstorage/azure_policy_1.png b/docs/remote_services/azureblobstorage/azure_policy_1.png new file mode 100644 index 0000000..6e6b482 --- /dev/null +++ b/docs/remote_services/azureblobstorage/azure_policy_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cba5bf27e76eaf8ea21e51e43172501fc4867539c90c1758a02945477dc261ea +size 132487 diff --git a/docs/remote_services/azureblobstorage/azure_policy_3.png b/docs/remote_services/azureblobstorage/azure_policy_3.png new file mode 100644 index 0000000..c1602e9 --- /dev/null +++ b/docs/remote_services/azureblobstorage/azure_policy_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b2add8a559eab0b7073f7ed52d4a3e4efae68b499c7b924d1ac50278758961b +size 193894 diff --git a/docs/remote_services/azureblobstorage/azure_rs_setting.png b/docs/remote_services/azureblobstorage/azure_rs_setting.png new file mode 100644 index 0000000..eb25f7d --- /dev/null +++ b/docs/remote_services/azureblobstorage/azure_rs_setting.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:582a69a7aee2cfab72dc14324e4a864acab92c145d533e799f8ceea61a8f0102 +size 478941 diff --git a/docs/remote_services/azureblobstorage/azure_sas_1.png b/docs/remote_services/azureblobstorage/azure_sas_1.png new file mode 100644 index 0000000..4a7c4cc --- /dev/null +++ b/docs/remote_services/azureblobstorage/azure_sas_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbb8863d98d8826739b5bf9fa4c7a2de3ba49fde4bbe3c4988a877ba74f89abd +size 143007 diff --git a/docs/remote_services/azureblobstorage/azure_sas_2.png b/docs/remote_services/azureblobstorage/azure_sas_2.png new file mode 100644 index 0000000..2470ad3 --- /dev/null +++ b/docs/remote_services/azureblobstorage/azure_sas_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddc30f97d75d7c3a6061cbb30cc991cbdf1f4039aad3547ef88998c3dd9be666 +size 132904 diff --git a/docs/services_connectable_or_not.md b/docs/services_connectable_or_not.md index 49676c5..4c1f93c 100644 --- a/docs/services_connectable_or_not.md +++ b/docs/services_connectable_or_not.md @@ -17,6 +17,7 @@ The list is for information purposes only. | [filebase](https://filebase.com/) | Yes | Yes | | | | QingStor 青云 | ? | ? | | | | [MinIO](https://min.io/) | Yes | Yes | | | +| Azure Blob Storage | Yes (PRO) | | | Yes (PRO) | | [WsgiDAV](https://github.com/mar10/wsgidav) | Yes | | Yes | | | [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes | | Yes | | | NextCloud | Yes | | Yes | | diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 1c3c38e..5595d55 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -51,6 +51,7 @@ esbuild "http", "https", "vm", + // "process", // ...builtins ], inject: ["./esbuild.injecthelper.mjs"], @@ -63,24 +64,28 @@ esbuild minify: prod, outfile: "main.js", define: { - "process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`, - "process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`, - "process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`, - "process.env.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`, - "process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`, - "process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`, - "process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`, - "process.env.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`, - "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}"`, - "process.env.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`, - "process.env.DEFAULT_KOOFR_CLIENT_SECRET": `"${DEFAULT_KOOFR_CLIENT_SECRET}"`, + "global.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`, + "global.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`, + "global.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`, + "global.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`, + "global.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`, + "global.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`, + "global.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`, + "global.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`, + "global.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`, + "global.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`, + "global.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`, + "global.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`, + "global.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`, + "global.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`, + "global.DEFAULT_KOOFR_CLIENT_SECRET": `"${DEFAULT_KOOFR_CLIENT_SECRET}"`, global: "window", "process.env.NODE_DEBUG": `undefined`, // ugly fix "process.env.DEBUG": `undefined`, // ugly fix + // "process.version": `"v20.10.0"`, // who's using this? + // "process":`undefined`, + // "global.process":`undefined`, + "globalThis.process": `undefined`, // make azure blob storage happy }, plugins: [inlineWorkerPlugin()], }) diff --git a/esbuild.injecthelper.mjs b/esbuild.injecthelper.mjs index cdb9664..ff6aa56 100644 --- a/esbuild.injecthelper.mjs +++ b/esbuild.injecthelper.mjs @@ -1,2 +1,2 @@ export const Buffer = require("buffer").Buffer; -export const process = require("process/browser"); +// export const process = require("process/browser"); diff --git a/package.json b/package.json index 5ae2e28..e50e80a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "browser": { "path": "path-browserify", - "process": "process/browser", + "process": false, "stream": "stream-browserify", "crypto": "crypto-browserify", "url": "url/", @@ -61,6 +61,7 @@ "@aws-sdk/signature-v4-crt": "^3.556.0", "@aws-sdk/types": "^3.535.0", "@azure/msal-node": "^2.7.0", + "@azure/storage-blob": "^12.23.0", "@fyears/rclone-crypt": "^0.0.7", "@fyears/tsqueue": "^1.0.1", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/pro/src/account.ts b/pro/src/account.ts index c048cce..9a44742 100644 --- a/pro/src/account.ts +++ b/pro/src/account.ts @@ -290,6 +290,11 @@ export const checkProRunnableAndFixInplace = async ( service: "koofr", name: "Koofr", }, + { + feature: "feature-azure_blob_storage", + service: "azureblobstorage", + name: "Azure Blob Storage", + }, ]; for (const { feature, service, name } of toChecked) { diff --git a/pro/src/baseTypesPro.ts b/pro/src/baseTypesPro.ts index 5ad40b2..81a3ee5 100644 --- a/pro/src/baseTypesPro.ts +++ b/pro/src/baseTypesPro.ts @@ -1,10 +1,29 @@ +//////////////////////////////////////////////////////////// +// hacks +//////////////////////////////////////////////////////////// + +declare global { + var DEFAULT_REMOTELYSAVE_WEBSITE: string; + var DEFAULT_REMOTELYSAVE_CLIENT_ID: string; + var DEFAULT_GOOGLEDRIVE_CLIENT_ID: string; + var DEFAULT_GOOGLEDRIVE_CLIENT_SECRET: string; + var DEFAULT_BOX_CLIENT_ID: string; + var DEFAULT_BOX_CLIENT_SECRET: string; + var DEFAULT_PCLOUD_CLIENT_ID: string; + var DEFAULT_PCLOUD_CLIENT_SECRET: string; + var DEFAULT_YANDEXDISK_CLIENT_ID: string; + var DEFAULT_YANDEXDISK_CLIENT_SECRET: string; + var DEFAULT_KOOFR_CLIENT_ID: string; + var DEFAULT_KOOFR_CLIENT_SECRET: string; +} + /////////////////////////////////////////////////////////// // PRO ////////////////////////////////////////////////////////// export const COMMAND_CALLBACK_PRO = "remotely-save-cb-pro"; -export const PRO_CLIENT_ID = process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID; -export const PRO_WEBSITE = process.env.DEFAULT_REMOTELYSAVE_WEBSITE; +export const PRO_CLIENT_ID = global.DEFAULT_REMOTELYSAVE_CLIENT_ID; +export const PRO_WEBSITE = global.DEFAULT_REMOTELYSAVE_WEBSITE; export type PRO_FEATURE_TYPE = | "feature-smart_conflict" @@ -12,7 +31,8 @@ export type PRO_FEATURE_TYPE = | "feature-box" | "feature-pcloud" | "feature-yandex_disk" - | "feature-koofr"; + | "feature-koofr" + | "feature-azure_blob_storage"; export interface FeatureInfo { featureName: PRO_FEATURE_TYPE; @@ -51,18 +71,17 @@ export interface GoogleDriveConfig { kind: "googledrive"; } -export const DEFAULT_GOOGLEDRIVE_CLIENT_ID = - process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID; -export const DEFAULT_GOOGLEDRIVE_CLIENT_SECRET = - process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET; +export const GOOGLEDRIVE_CLIENT_ID = global.DEFAULT_GOOGLEDRIVE_CLIENT_ID; +export const GOOGLEDRIVE_CLIENT_SECRET = + global.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET; /////////////////////////////////////////////////////////// // box ////////////////////////////////////////////////////////// export const COMMAND_CALLBACK_BOX = "remotely-save-cb-box"; -export const BOX_CLIENT_ID = process.env.DEFAULT_BOX_CLIENT_ID; -export const BOX_CLIENT_SECRET = process.env.DEFAULT_BOX_CLIENT_SECRET; +export const BOX_CLIENT_ID = global.DEFAULT_BOX_CLIENT_ID; +export const BOX_CLIENT_SECRET = global.DEFAULT_BOX_CLIENT_SECRET; export interface BoxConfig { accessToken: string; @@ -79,8 +98,8 @@ export interface BoxConfig { ////////////////////////////////////////////////////////// export const COMMAND_CALLBACK_PCLOUD = "remotely-save-cb-pcloud"; -export const PCLOUD_CLIENT_ID = process.env.DEFAULT_PCLOUD_CLIENT_ID; -export const PCLOUD_CLIENT_SECRET = process.env.DEFAULT_PCLOUD_CLIENT_SECRET; +export const PCLOUD_CLIENT_ID = global.DEFAULT_PCLOUD_CLIENT_ID; +export const PCLOUD_CLIENT_SECRET = global.DEFAULT_PCLOUD_CLIENT_SECRET; export interface PCloudConfig { accessToken: string; @@ -101,9 +120,8 @@ export interface PCloudConfig { ////////////////////////////////////////////////////////// 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 const YANDEXDISK_CLIENT_ID = global.DEFAULT_YANDEXDISK_CLIENT_ID; +export const YANDEXDISK_CLIENT_SECRET = global.DEFAULT_YANDEXDISK_CLIENT_SECRET; export interface YandexDiskConfig { accessToken: string; @@ -121,8 +139,8 @@ export interface YandexDiskConfig { ////////////////////////////////////////////////////////// export const COMMAND_CALLBACK_KOOFR = "remotely-save-cb-koofr"; -export const KOOFR_CLIENT_ID = process.env.DEFAULT_KOOFR_CLIENT_ID; -export const KOOFR_CLIENT_SECRET = process.env.DEFAULT_KOOFR_CLIENT_SECRET; +export const KOOFR_CLIENT_ID = global.DEFAULT_KOOFR_CLIENT_ID; +export const KOOFR_CLIENT_SECRET = global.DEFAULT_KOOFR_CLIENT_SECRET; export interface KoofrConfig { accessToken: string; @@ -136,3 +154,16 @@ export interface KoofrConfig { mountID: string; kind: "koofr"; } + +/////////////////////////////////////////////////////////// +// Azure Blob Storage +////////////////////////////////////////////////////////// + +export interface AzureBlobStorageConfig { + containerSasUrl: string; + containerName: string; + remotePrefix: string; + generateFolderObject: boolean; + partsConcurrency: number; + kind: "azureblobstorage"; +} diff --git a/pro/src/fsAzureBlobStorage.ts b/pro/src/fsAzureBlobStorage.ts new file mode 100644 index 0000000..ead113d --- /dev/null +++ b/pro/src/fsAzureBlobStorage.ts @@ -0,0 +1,342 @@ +import * as path from "path"; +import { + AnonymousCredential, + BaseRequestPolicy, + type BlobGetPropertiesResponse, + BlobServiceClient, + BlobUploadCommonResponse, + BlockBlobClient, + ContainerClient, + newPipeline, +} from "@azure/storage-blob"; +import type { Entity } from "../../src/baseTypes"; +import { FakeFs } from "../../src/fsAll"; +import { arrayBufferToHex, getFolderLevels } from "../../src/misc"; +import type { AzureBlobStorageConfig } from "./baseTypesPro"; + +export const simpleTransRemotePrefix = (x: string) => { + if (x === undefined) { + return ""; + } + let y = path.posix.normalize(x.trim()); + if (y === undefined || y === "" || y === "/" || y === ".") { + return ""; + } + if (y.startsWith("/")) { + y = y.slice(1); + } + if (!y.endsWith("/")) { + y = `${y}/`; + } + return y; +}; + +export const DEFAULT_AZUREBLOBSTORAGE_CONFIG: AzureBlobStorageConfig = { + containerSasUrl: "", + containerName: "", + remotePrefix: "", + generateFolderObject: false, + partsConcurrency: 5, + kind: "azureblobstorage", +}; + +const getNormPath = (fileOrFolderPath: string, remotePrefix: string) => { + if (remotePrefix.startsWith("/") || !remotePrefix.endsWith("/")) { + throw Error( + `remotePrefix should not have leading slash but should have tailing slash: ${remotePrefix}` + ); + } + if (!fileOrFolderPath.startsWith(remotePrefix)) { + throw Error(`${fileOrFolderPath} does not start with ${remotePrefix}!`); + } + return fileOrFolderPath.slice(remotePrefix.length); +}; + +const getBlobPath = (fileOrFolderPath: string, remotePrefix: string) => { + if (remotePrefix.startsWith("/") || !remotePrefix.endsWith("/")) { + throw Error( + `remotePrefix should not have leading slash but should have tailing slash: ${remotePrefix}` + ); + } + return `${remotePrefix}${fileOrFolderPath}`; +}; + +const fromBlobPropsToEntity = ( + name: string, + props: BlobGetPropertiesResponse, + remotePrefix: string +): Entity => { + const key = getNormPath(name, remotePrefix); + + let mtimeCli = props.lastModified!.valueOf(); + const mtimeStr = props.metadata?.mtime; + if (mtimeStr !== undefined && mtimeStr !== "") { + try { + mtimeCli = new Date(mtimeStr).valueOf(); + } catch {} + } + + let hash: undefined | string = undefined; + if (props.contentMD5 !== undefined) { + hash = arrayBufferToHex(props.contentMD5.buffer); + } + + const entity: Entity = { + key: key, + keyRaw: key, + mtimeCli: mtimeCli, + mtimeSvr: props.lastModified!.valueOf(), + size: props.contentLength ?? 0, + sizeRaw: props.contentLength ?? 0, + hash: hash, + }; + + if (key.endsWith("/")) { + entity.synthesizedFolder = false; + } + + return entity; +}; + +export class FakeFsAzureBlobStorage extends FakeFs { + kind: string; + config: AzureBlobStorageConfig; + vaultName: string; + containerClient: ContainerClient; + remotePrefix: string; + synthFoldersCache: Record; + + constructor(config: AzureBlobStorageConfig, vaultName: string) { + super(); + this.kind = "azureblobstorage"; + this.config = config; + this.vaultName = vaultName; + this.synthFoldersCache = {}; + + this.remotePrefix = `${vaultName}/`; + const k = simpleTransRemotePrefix(this.config.remotePrefix); + if (k !== "") { + // we have prefix + this.remotePrefix = k; + } + + this.containerClient = new ContainerClient(this.config.containerSasUrl); + } + + async walk(): Promise { + const entities: Entity[] = []; + const realEntities = new Set(); + for await (const blob of this.containerClient.listBlobsFlat({ + prefix: this.remotePrefix, + includeMetadata: true, + })) { + const blockBlobClient = this.containerClient.getBlockBlobClient( + blob.name + ); + const props = await blockBlobClient.getProperties(); + + // console.debug(blob.name) + + const entity = fromBlobPropsToEntity(blob.name, props, this.remotePrefix); + entities.push(entity); + + // so we need to fake the folders + realEntities.add(entity.key!); + for (const f of getFolderLevels(entity.key!, true)) { + if (realEntities.has(f)) { + delete this.synthFoldersCache[f]; + continue; + } + if ( + !this.synthFoldersCache.hasOwnProperty(f) || + entity.mtimeSvr! >= this.synthFoldersCache[f].mtimeSvr! + ) { + this.synthFoldersCache[f] = { + key: f, + keyRaw: f, + size: 0, + sizeRaw: 0, + sizeEnc: 0, + mtimeSvr: entity.mtimeSvr, + mtimeSvrFmt: entity.mtimeSvrFmt, + mtimeCli: entity.mtimeCli, + mtimeCliFmt: entity.mtimeCliFmt, + synthesizedFolder: true, + }; + } + } + } + for (const key of Object.keys(this.synthFoldersCache)) { + entities.push(this.synthFoldersCache[key]); + } + return entities; + } + + async walkPartial(): Promise { + const entities: Entity[] = []; + for await (const blob of this.containerClient.listBlobsByHierarchy("/", { + prefix: this.remotePrefix, + includeMetadata: true, + })) { + if (blob.kind === "prefix") { + continue; + } + const blockBlobClient = this.containerClient.getBlockBlobClient( + blob.name + ); + const props = await blockBlobClient.getProperties(); + + const entity = fromBlobPropsToEntity(blob.name, props, this.remotePrefix); + entities.push(entity); + } + return entities; + } + + async stat(key: string): Promise { + const remotePath = getBlobPath(key, this.remotePrefix); + const blockBlobClient = this.containerClient.getBlockBlobClient(remotePath); + const props = await blockBlobClient.getProperties(); + + const entity = fromBlobPropsToEntity(remotePath, props, this.remotePrefix); + return entity; + } + + async mkdir( + key: string, + mtime?: number | undefined, + ctime?: number | undefined + ): Promise { + if (!key.endsWith("/")) { + throw new Error(`You should not call mkdir on ${key}!`); + } + + const generateFolderObject = this.config.generateFolderObject ?? false; + if (!generateFolderObject) { + const synth = { + key: key, + keyRaw: key, + size: 0, + sizeRaw: 0, + sizeEnc: 0, + mtimeSvr: mtime, + mtimeCli: mtime, + synthesizedFolder: true, + }; + this.synthFoldersCache[key] = synth; + return synth; + } + + return await this.writeFile( + key, + new ArrayBuffer(0), + mtime ?? Date.now(), + ctime ?? Date.now() + ); + } + + async writeFile( + key: string, + content: ArrayBuffer, + mtime: number, + ctime: number + ): Promise { + const blobPath = getBlobPath(key, this.remotePrefix); + + const blobClient = this.containerClient.getBlockBlobClient(blobPath); + const metadata: Record = { + mtime: new Date(mtime).toISOString(), + ctime: new Date(ctime).toISOString(), + }; + + if (key.endsWith("/")) { + console.debug(`yeah we have folder upload`); + const generateFolderObject = this.config.generateFolderObject ?? false; + if (!generateFolderObject) { + throw Error( + `if not generate folder object, the func should not go here` + ); + } + metadata["hdi_isfolder"] = "true"; + } + + const uploadResult = await blobClient.uploadData(content, { + metadata: metadata, + concurrency: this.config.partsConcurrency ?? 5, + }); + + if (key.endsWith("/")) { + console.debug(`yeah we have folder upload`); + console.debug(uploadResult); + } + + if (uploadResult._response.status >= 300) { + throw Error(`upload ${key} failed with ${JSON.stringify(uploadResult)}`); + } + + return await this.stat(key); + } + + async readFile(key: string): Promise { + const blobPath = getBlobPath(key, this.remotePrefix); + const blobClient = this.containerClient.getBlockBlobClient(blobPath); + const rsp = await blobClient.download(); + if (rsp._response.status >= 300) { + throw Error(`download ${key} failed with ${JSON.stringify(rsp)}`); + } + return await (await rsp.blobBody)!.arrayBuffer(); + } + + async rename(key1: string, key2: string): Promise { + throw new Error("Method not implemented."); + } + + async rm(key: string): Promise { + const blobPath = getBlobPath(key, this.remotePrefix); + if (key.endsWith("/")) { + if (this.synthFoldersCache.hasOwnProperty(key)) { + delete this.synthFoldersCache[key]; + } + + // in blob the folder may not exist, so we make our best effort. + // do NOT read this.config.generateFolderObject + // because the folder might be generated by previous setting + try { + const blobClient = this.containerClient.getBlockBlobClient(blobPath); + await blobClient.deleteIfExists(); + } catch (e) {} + } else { + // the file should really exist + const blobClient = this.containerClient.getBlockBlobClient(blobPath); + const rsp = await blobClient.deleteIfExists(); + if (!rsp.succeeded) { + throw Error( + `something goes wrong while deleting ${key}: ${JSON.stringify(rsp)}` + ); + } + } + } + + async checkConnect(callbackFunc?: any): Promise { + // if we can walk, we can connect + try { + await this.walkPartial(); + return true; + } catch (err) { + console.debug(err); + callbackFunc?.(err); + return false; + } + } + + async getUserDisplayName(): Promise { + throw new Error("Method not implemented."); + } + + async revokeAuth(): Promise { + throw new Error("Method not implemented."); + } + + allowEmptyFile(): boolean { + return true; + } +} diff --git a/pro/src/fsGoogleDrive.ts b/pro/src/fsGoogleDrive.ts index 392c0f1..91a0f29 100644 --- a/pro/src/fsGoogleDrive.ts +++ b/pro/src/fsGoogleDrive.ts @@ -14,8 +14,8 @@ import { unixTimeToStr, } from "../../src/misc"; import { - DEFAULT_GOOGLEDRIVE_CLIENT_ID, - DEFAULT_GOOGLEDRIVE_CLIENT_SECRET, + GOOGLEDRIVE_CLIENT_ID, + GOOGLEDRIVE_CLIENT_SECRET, type GoogleDriveConfig, } from "./baseTypesPro"; @@ -103,8 +103,8 @@ export const sendRefreshTokenReq = async (refreshToken: string) => { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ - client_id: DEFAULT_GOOGLEDRIVE_CLIENT_ID ?? "", - client_secret: DEFAULT_GOOGLEDRIVE_CLIENT_SECRET ?? "", + client_id: GOOGLEDRIVE_CLIENT_ID ?? "", + client_secret: GOOGLEDRIVE_CLIENT_SECRET ?? "", grant_type: "refresh_token", refresh_token: refreshToken, }).toString(), diff --git a/pro/src/langs/en.json b/pro/src/langs/en.json index da83a52..e43758f 100644 --- a/pro/src/langs/en.json +++ b/pro/src/langs/en.json @@ -92,6 +92,13 @@ "modal_koofrrevokeauth_clean_notice": "Cleaned!", "modal_koofrrevokeauth_clean_fail": "Something goes wrong while revoking.", + "modal_remoteprefix_azureblobstorage_title": "You are changing the remote prefix config", + "modal_remoteprefix_azureblobstorage_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the prefix will be the vault name.\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.", + "modal_remoteprefix_azureblobstorage_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.", + "modal_remoteprefix_azureblobstorage_tosave": "The prefix to save is \"{{{prefix}}}\"", + "modal_remoteprefix_azureblobstorage_secondconfirm_change": "Confirm To Change", + "modal_remoteprefix_azureblobstorage_notice": "New remote prefix config saved!", + "modal_prorevokeauth": "Revoke auth by clicking here and follow the steps.", "modal_prorevokeauth_clean": "Clean", "modal_prorevokeauth_clean_desc": "Clean local auth record", @@ -199,11 +206,36 @@ "settings_koofr_connect_succ": "Great! We can connect to Koofr!", "settings_koofr_connect_fail": "We cannot connect to Koofr.", + "settings_azureblobstorage": "Azure Blob Storage (PRO) (alpha)", + "settings_chooseservice_azureblobstorage": "Azure Blob Storage (PRO) (alpha)", + "settings_azureblobstorage_disclaimer1": "Disclaimer: This app is NOT an official Microsoft / Azure product. The app just uses Azure Blob Storage's public api.", + "settings_azureblobstorage_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Azure Blob Storage, please immediately delete the SAS Url and try to change your policy or account info.", + "settings_azureblobstorage_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_azureblobstorage_notshowuphint": "Azure Blob Storage Settings Not Available", + "settings_azureblobstorage_notshowuphint_desc": "Azure Blob Storage settings are not available, because you haven't subscribed to the PRO feature in your Remotely Save account.", + "settings_azureblobstorage_notshowuphint_view_pro": "View PRO Settings", + "settings_azureblobstorage_folder": "We will create and sync inside the prefix {{remoteBaseDir}} on your Azure Blob Storage.", + "settings_azureblobstorage_containersasurl": "Container SAS Url", + "settings_azureblobstorage_containersasurl_desc": "The only connction method is providing the Container SAS Url. Remember to configure CORS as well. Please read the doc for details.", + "settings_azureblobstorage_containername": "Container Name", + "settings_azureblobstorage_containername_desc": "Input your container name here.", + "settings_azureblobstorage_remoteprefix": "Remote Prefix", + "settings_azureblobstorage_remoteprefix_desc": "Input your remote prefix of blobs for this vault. If you set this value to empty, the vault name will be used as prefix. Empty prefix is not allowed and not settable here.", + "settings_azureblobstorage_parts": "Parts Concurrency", + "settings_azureblobstorage_parts_desc": "Large files are split into small parts to upload. How many parts do you want to upload in parallel at most?", + "settings_azureblobstorage_generatefolderobject": "Generate Folder Object Or Not", + "settings_azureblobstorage_generatefolderobject_desc": "Azure Blob Storage doesn't have \"real\" folder. If you set \"Generate\" here, the plugin will upload a zero-byte object endding with \"/\" to represent the folder. By default, the plugin skips generating folder object.", + "settings_azureblobstorage_generatefolderobject_notgenerate": "Not generate (default)", + "settings_azureblobstorage_generatefolderobject_generate": "Generate", + "settings_azureblobstorage_connect_succ": "Great! We can connect to Azure Blob Storage!", + "settings_azureblobstorage_connect_fail": "We cannot connect to Azure Blob Storage.", + "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_export_koofr_button": "Export Koofr Part", + "settings_export_azureblobstorage_button": "Export Azure Blob Storage 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 022406f..ec0c980 100644 --- a/pro/src/langs/zh_cn.json +++ b/pro/src/langs/zh_cn.json @@ -102,6 +102,14 @@ "modal_koofrrevokeauth_clean_notice": "已清理!", "modal_koofrrevokeauth_clean_fail": "清理授权时候发生了错误。", + "modal_remoteprefix_azureblobstorage_title": "您正在修改远端路径前缀设置", + "modal_remoteprefix_azureblobstorage_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置为保存为空,文件将会使用 Vault 名字作为前缀。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。", + "modal_remoteprefix_azureblobstorage_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。", + "modal_remoteprefix_azureblobstorage_tosave": "您设定的新前缀为:“{{{prefix}}}”", + "modal_remoteprefix_azureblobstorage_secondconfirm_empty": "前缀为空,文件会保存在根目录", + "modal_remoteprefix_azureblobstorage_secondconfirm_change": "确认修改", + "modal_remoteprefix_azureblobstorage_notice": "新的远端路径前缀设置已保存!", + "modal_prorevokeauth": "点击这里和按照步骤取消授权。", "modal_prorevokeauth_clean": "清理", "modal_prorevokeauth_clean_desc": "清理本地授权记录", @@ -209,11 +217,36 @@ "settings_koofr_connect_succ": "很好!我们可连接上 Koofr!", "settings_koofr_connect_fail": "我们未能连接上 Koofr。", + "settings_azureblobstorage": "Azure Blob Storage (PRO) (alpha)", + "settings_chooseservice_azureblobstorage": "Azure Blob Storage (PRO) (alpha)", + "settings_azureblobstorage_disclaimer1": "声明:本插件不是微软或 Azure 的官方产品。只是用到了 Azure 的公开 API。", + "settings_azureblobstorage_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现任何不符合预期的 Azure Blob Storage 访问,请立刻删除 SAS Url 然后修改预设的 policy 或账号信息。", + "settings_azureblobstorage_pro_desc": "

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

", + "settings_azureblobstorage_notshowuphint": "Azure Blob Storage 设置不可用", + "settings_azureblobstorage_notshowuphint_desc": "Azure Blob Storage 设置不可用,因为您没有在 Remotely Save 账号里开启这个 PRO 功能。", + "settings_azureblobstorage_notshowuphint_view_pro": "查看 PRO 相关设置", + "settings_azureblobstorage_folder": "Azure Blob Storage 里,我们会用这个前缀保存你的文件: {{remoteBaseDir}} 。", + "settings_azureblobstorage_containersasurl": "Container SAS Url", + "settings_azureblobstorage_containersasurl_desc": "目前只支持通过 Container SAS Url 进行连接。请记得同时设置 CORS。可阅读文档获取更多说明。", + "settings_azureblobstorage_containername": "Container 名字", + "settings_azureblobstorage_containername_desc": "输入 Container 名字。", + "settings_azureblobstorage_remoteprefix": "修改远端前缀路径", + "settings_azureblobstorage_remoteprefix_desc": "您可以在这里修改路径前缀。如果设置为空,那么库(vault)名字会作为前缀。不允许设置空前缀。您需要点击“确认”。", + "settings_azureblobstorage_parts": "分块并行度", + "settings_azureblobstorage_parts_desc": "大文件会被分块上传。您希望同一时间最多有多少个分块被上传?", + "settings_azureblobstorage_generatefolderobject": "是否生成文件夹 Object", + "settings_azureblobstorage_generatefolderobject_desc": "Azure Blob Storage 不存在“真正”的文件夹。如果您设置了“生成”,那么插件会上传 0 字节的以“/”结尾的 Object 来代表文件夹。默认跳过生成这种文件夹 Object。", + "settings_azureblobstorage_generatefolderobject_notgenerate": "不生成(默认)", + "settings_azureblobstorage_generatefolderobject_generate": "生成", + "settings_azureblobstorage_connect_succ": "很好!我们可连接上 Azure Blob Storage!", + "settings_azureblobstorage_connect_fail": "我们未能连接上 Azure Blob Storage。", + "settings_export_googledrive_button": "导出 Google Drive 部分", "settings_export_box_button": "导出 Box 部分", "settings_export_pcloud_button": "导出 pCloud 部分", "settings_export_yandexdisk_button": "导出 Yandex Disk 部分", "settings_export_koofr_button": "导出 Koofr 部分", + "settings_export_azureblobstorage_button": "导出 Azure Blob Storage 部分", "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 6a43572..08ef34c 100644 --- a/pro/src/langs/zh_tw.json +++ b/pro/src/langs/zh_tw.json @@ -102,6 +102,14 @@ "modal_koofrrevokeauth_clean_notice": "已清理!", "modal_koofrrevokeauth_clean_fail": "清理授權時候發生了錯誤。", + "modal_remoteprefix_azureblobstorage_title": "您正在修改遠端路徑字首設定", + "modal_remoteprefix_azureblobstorage_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定為儲存為空,檔案將會使用 Vault 名字作為字首。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。", + "modal_remoteprefix_azureblobstorage_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。", + "modal_remoteprefix_azureblobstorage_tosave": "您設定的新字首為:“{{{prefix}}}”", + "modal_remoteprefix_azureblobstorage_secondconfirm_empty": "字首為空,檔案會儲存在根目錄", + "modal_remoteprefix_azureblobstorage_secondconfirm_change": "確認修改", + "modal_remoteprefix_azureblobstorage_notice": "新的遠端路徑字首設定已儲存!", + "modal_prorevokeauth": "點選這裡和按照步驟取消授權。", "modal_prorevokeauth_clean": "清理", "modal_prorevokeauth_clean_desc": "清理本地授權記錄", @@ -209,11 +217,36 @@ "settings_koofr_connect_succ": "很好!我們可連線上 Koofr!", "settings_koofr_connect_fail": "我們未能連線上 Koofr。", + "settings_azureblobstorage": "Azure Blob Storage (PRO) (alpha)", + "settings_chooseservice_azureblobstorage": "Azure Blob Storage (PRO) (alpha)", + "settings_azureblobstorage_disclaimer1": "宣告:本外掛不是微軟或 Azure 的官方產品。只是用到了 Azure 的公開 API。", + "settings_azureblobstorage_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現任何不符合預期的 Azure Blob Storage 訪問,請立刻刪除 SAS Url 然後修改預設的 policy 或賬號資訊。", + "settings_azureblobstorage_pro_desc": "

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

", + "settings_azureblobstorage_notshowuphint": "Azure Blob Storage 設定不可用", + "settings_azureblobstorage_notshowuphint_desc": "Azure Blob Storage 設定不可用,因為您沒有在 Remotely Save 賬號裡開啟這個 PRO 功能。", + "settings_azureblobstorage_notshowuphint_view_pro": "檢視 PRO 相關設定", + "settings_azureblobstorage_folder": "Azure Blob Storage 裡,我們會用這個字首儲存你的檔案: {{remoteBaseDir}} 。", + "settings_azureblobstorage_containersasurl": "Container SAS Url", + "settings_azureblobstorage_containersasurl_desc": "目前只支援透過 Container SAS Url 進行連線。請記得同時設定 CORS。可閱讀文件獲取更多說明。", + "settings_azureblobstorage_containername": "Container 名字", + "settings_azureblobstorage_containername_desc": "輸入 Container 名字。", + "settings_azureblobstorage_remoteprefix": "修改遠端字首路徑", + "settings_azureblobstorage_remoteprefix_desc": "您可以在這裡修改路徑字首。如果設定為空,那麼庫(vault)名字會作為字首。不允許設定空字首。您需要點選“確認”。", + "settings_azureblobstorage_parts": "分塊並行度", + "settings_azureblobstorage_parts_desc": "大檔案會被分塊上傳。您希望同一時間最多有多少個分塊被上傳?", + "settings_azureblobstorage_generatefolderobject": "是否生成資料夾 Object", + "settings_azureblobstorage_generatefolderobject_desc": "Azure Blob Storage 不存在“真正”的資料夾。如果您設定了“生成”,那麼外掛會上傳 0 位元組的以“/”結尾的 Object 來代表資料夾。預設跳過生成這種資料夾 Object。", + "settings_azureblobstorage_generatefolderobject_notgenerate": "不生成(預設)", + "settings_azureblobstorage_generatefolderobject_generate": "生成", + "settings_azureblobstorage_connect_succ": "很好!我們可連線上 Azure Blob Storage!", + "settings_azureblobstorage_connect_fail": "我們未能連線上 Azure Blob Storage。", + "settings_export_googledrive_button": "匯出 Google Drive 部分", "settings_export_box_button": "匯出 Box 部分", "settings_export_pcloud_button": "匯出 pCloud 部分", "settings_export_yandexdisk_button": "匯出 Yandex Disk 部分", "settings_export_koofr_button": "匯出 Koofr 部分", + "settings_export_azureblobstorage_button": "匯出 Azure Blob Storage 部分", "settings_pro": "賬號(PRO 付費功能)", "settings_pro_tutorial": "

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

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

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

第二部:點選“連線”按鈕,從而連線本裝置和線上賬號。", diff --git a/pro/src/settingsAzureBlobStorage.ts b/pro/src/settingsAzureBlobStorage.ts new file mode 100644 index 0000000..13f265b --- /dev/null +++ b/pro/src/settingsAzureBlobStorage.ts @@ -0,0 +1,293 @@ +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 { wrapTextWithPasswordHide } from "../../src/settings"; +import { + DEFAULT_AZUREBLOBSTORAGE_CONFIG, + simpleTransRemotePrefix, +} from "./fsAzureBlobStorage"; + +class ChangeAzureBlobStorageRemotePrefixModal extends Modal { + readonly plugin: RemotelySavePlugin; + readonly newRemotePrefix: string; + constructor(app: App, plugin: RemotelySavePlugin, newRemotePrefix: string) { + super(app); + this.plugin = plugin; + this.newRemotePrefix = newRemotePrefix; + } + + onOpen() { + const { contentEl } = this; + + const t = (x: TransItemType, vars?: any) => { + return this.plugin.i18n.t(x, vars); + }; + + contentEl.createEl("h2", { + text: t("modal_remoteprefix_azureblobstorage_title"), + }); + t("modal_remoteprefix_azureblobstorage_shortdesc") + .split("\n") + .forEach((val, idx) => { + contentEl.createEl("p", { + text: val, + }); + }); + + contentEl.createEl("p", { + text: t("modal_remoteprefix_azureblobstorage_tosave", { + prefix: this.newRemotePrefix, + }), + }); + + new Setting(contentEl) + .addButton((button) => { + button.setButtonText( + t("modal_remoteprefix_azureblobstorage_secondconfirm_change") + ); + button.onClick(async () => { + this.plugin.settings.azureblobstorage.remotePrefix = + this.newRemotePrefix; + await this.plugin.saveSettings(); + new Notice(t("modal_remoteprefix_azureblobstorage_notice")); + this.close(); + }); + button.setClass("remoteprefix-azureblobstorage-second-confirm"); + }) + .addButton((button) => { + button.setButtonText(t("goback")); + button.onClick(() => { + this.close(); + }); + }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} + +export const generateAzureBlobStorageSettingsPart = ( + containerEl: HTMLElement, + t: (x: TransItemType, vars?: any) => string, + app: App, + plugin: RemotelySavePlugin, + saveUpdatedConfigFunc: () => Promise | undefined +) => { + const azureBlobStorageDiv = containerEl.createEl("div", { + cls: "azureblobstorage-hide", + }); + azureBlobStorageDiv.toggleClass( + "azureblobstorage-hide", + plugin.settings.serviceType !== "azureblobstorage" + ); + azureBlobStorageDiv.createEl("h2", { text: t("settings_azureblobstorage") }); + + const azureBlobStorageLongDescDiv = azureBlobStorageDiv.createEl("div", { + cls: "settings-long-desc", + }); + for (const c of [ + t("settings_azureblobstorage_disclaimer1"), + stringToFragment(t("settings_azureblobstorage_disclaimer2")), + ]) { + azureBlobStorageLongDescDiv.createEl("p", { + text: c, + cls: "azureblobstorage-disclaimer", + }); + } + + azureBlobStorageLongDescDiv.createEl("p", { + text: t("settings_azureblobstorage_folder", { + remotePrefix: + plugin.settings.azureblobstorage.remotePrefix || + `${app.vault.getName()}/`, + }), + }); + + azureBlobStorageLongDescDiv.createDiv({ + text: stringToFragment(t("settings_azureblobstorage_pro_desc")), + cls: "azureblobstorage-disclaimer", + }); + + const azureBlobStorageNotShowUpHintSetting = new Setting(azureBlobStorageDiv) + .setName(t("settings_azureblobstorage_notshowuphint")) + .setDesc(t("settings_azureblobstorage_notshowuphint_desc")) + .addButton(async (button) => { + button.setButtonText( + t("settings_azureblobstorage_notshowuphint_view_pro") + ); + button.onClick(async () => { + window.location.href = "#settings-pro"; + }); + }); + + const azureBlobStorageAllowedToUsedDiv = azureBlobStorageDiv.createDiv(); + // if pro enabled, show up; otherwise hide. + const allowAzureBlobStorage = + plugin.settings.pro?.enabledProFeatures.filter( + (x) => x.featureName === "feature-azure_blob_storage" + ).length === 1; + console.debug( + `allow to show up azureBlobStorage settings? ${allowAzureBlobStorage}` + ); + if (allowAzureBlobStorage) { + azureBlobStorageAllowedToUsedDiv.removeClass( + "azureblobstorage-allow-to-use-hide" + ); + azureBlobStorageNotShowUpHintSetting.settingEl.addClass( + "azureblobstorage-allow-to-use-hide" + ); + } else { + azureBlobStorageAllowedToUsedDiv.addClass( + "azureblobstorage-allow-to-use-hide" + ); + azureBlobStorageNotShowUpHintSetting.settingEl.removeClass( + "azureblobstorage-allow-to-use-hide" + ); + } + + new Setting(azureBlobStorageAllowedToUsedDiv) + .setName(t("settings_azureblobstorage_containersasurl")) + .setDesc( + stringToFragment(t("settings_azureblobstorage_containersasurl_desc")) + ) + .addText((text) => { + wrapTextWithPasswordHide(text); + text + .setPlaceholder("") + .setValue(`${plugin.settings.azureblobstorage.containerSasUrl}`) + .onChange(async (value) => { + plugin.settings.azureblobstorage.containerSasUrl = value.trim(); + await plugin.saveSettings(); + }); + }); + + new Setting(azureBlobStorageAllowedToUsedDiv) + .setName(t("settings_azureblobstorage_containername")) + .setDesc(t("settings_azureblobstorage_containername_desc")) + .addText((text) => { + wrapTextWithPasswordHide(text); + text + .setPlaceholder("") + .setValue(`${plugin.settings.azureblobstorage.containerName}`) + .onChange(async (value) => { + plugin.settings.azureblobstorage.containerName = value.trim(); + await plugin.saveSettings(); + }); + }); + + let newAzureBlobStorageRemotePrefix = + plugin.settings.azureblobstorage.remotePrefix || ""; + new Setting(azureBlobStorageAllowedToUsedDiv) + .setName(t("settings_azureblobstorage_remoteprefix")) + .setDesc(t("settings_azureblobstorage_remoteprefix_desc")) + .addText((text) => + text + .setPlaceholder(`${app.vault.getName()}/`) + .setValue(newAzureBlobStorageRemotePrefix) + .onChange((value) => { + const k = simpleTransRemotePrefix(value); + if (k === "") { + newAzureBlobStorageRemotePrefix = `${app.vault.getName()}/`; + } else { + newAzureBlobStorageRemotePrefix = k; + } + }) + ) + .addButton((button) => { + button.setButtonText(t("confirm")); + button.onClick(() => { + new ChangeAzureBlobStorageRemotePrefixModal( + app, + plugin, + newAzureBlobStorageRemotePrefix + ).open(); + }); + }); + + new Setting(azureBlobStorageAllowedToUsedDiv) + .setName(t("settings_azureblobstorage_parts")) + .setDesc(t("settings_azureblobstorage_parts_desc")) + .addDropdown((dropdown) => { + dropdown.addOption("1", "1"); + dropdown.addOption("2", "2"); + dropdown.addOption("3", "3"); + dropdown.addOption("5", "5"); + dropdown.addOption("10", "10"); + dropdown.addOption("15", "15"); + dropdown.addOption("20", "20 (default)"); + + dropdown + .setValue(`${plugin.settings.azureblobstorage.partsConcurrency}`) + .onChange(async (val) => { + const realVal = Number.parseInt(val); + plugin.settings.azureblobstorage.partsConcurrency = realVal; + await plugin.saveSettings(); + }); + }); + + new Setting(azureBlobStorageAllowedToUsedDiv) + .setName(t("settings_azureblobstorage_generatefolderobject")) + .setDesc(t("settings_azureblobstorage_generatefolderobject_desc")) + .addDropdown((dropdown) => { + dropdown + .addOption( + "notgenerate", + t("settings_azureblobstorage_generatefolderobject_notgenerate") + ) + .addOption( + "generate", + t("settings_azureblobstorage_generatefolderobject_generate") + ); + + dropdown + .setValue( + `${ + plugin.settings.azureblobstorage.generateFolderObject + ? "generate" + : "notgenerate" + }` + ) + .onChange(async (val) => { + if (val === "generate") { + plugin.settings.azureblobstorage.generateFolderObject = true; + } else { + plugin.settings.azureblobstorage.generateFolderObject = false; + } + await plugin.saveSettings(); + }); + }); + + new Setting(azureBlobStorageAllowedToUsedDiv) + .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_azureblobstorage_connect_succ")); + } else { + new Notice(t("settings_azureblobstorage_connect_fail")); + new Notice(errors.msg); + } + }); + }); + + return { + azureBlobStorageDiv: azureBlobStorageDiv, + azureBlobStorageAllowedToUsedDiv: azureBlobStorageAllowedToUsedDiv, + azureBlobStorageNotShowUpHintSetting: azureBlobStorageNotShowUpHintSetting, + }; +}; diff --git a/pro/src/settingsPro.ts b/pro/src/settingsPro.ts index d6e61b3..8140a49 100644 --- a/pro/src/settingsPro.ts +++ b/pro/src/settingsPro.ts @@ -257,7 +257,9 @@ export const generateProSettingsPart = ( yandexDiskAllowedToUsedDiv: HTMLDivElement, yandexDiskNotShowUpHintSetting: Setting, koofrAllowedToUsedDiv: HTMLDivElement, - koofrNotShowUpHintSetting: Setting + koofrNotShowUpHintSetting: Setting, + azureBlobStorageAllowedToUsedDiv: HTMLDivElement, + azureBlobStorageNotShowUpHintSetting: Setting ) => { proDiv .createEl("h2", { text: t("settings_pro") }) @@ -389,6 +391,29 @@ export const generateProSettingsPart = ( ); } + const allowAzureBlobStorage = + plugin.settings.pro?.enabledProFeatures.filter( + (x) => x.featureName === "feature-azure_blob_storage" + ).length === 1; + console.debug( + `allow to show up AzureBlobStorage settings? ${allowAzureBlobStorage}` + ); + if (allowAzureBlobStorage) { + azureBlobStorageAllowedToUsedDiv.removeClass( + "azureblobstorage-allow-to-use-hide" + ); + azureBlobStorageNotShowUpHintSetting.settingEl.addClass( + "azureBlobStorage-allow-to-use-hide" + ); + } else { + azureBlobStorageAllowedToUsedDiv.addClass( + "azureblobstorage-allow-to-use-hide" + ); + azureBlobStorageNotShowUpHintSetting.settingEl.removeClass( + "azureblobstorage-allow-to-use-hide" + ); + } + new Notice(t("settings_pro_features_refresh_succ")); }); }); diff --git a/src/baseTypes.ts b/src/baseTypes.ts index fe9a545..f87216a 100644 --- a/src/baseTypes.ts +++ b/src/baseTypes.ts @@ -4,6 +4,7 @@ */ import type { + AzureBlobStorageConfig, BoxConfig, GoogleDriveConfig, KoofrConfig, @@ -13,6 +14,16 @@ import type { } from "../pro/src/baseTypesPro"; import type { LangTypeAndAuto } from "./i18n"; +declare global { + var DEFAULT_DROPBOX_APP_KEY: string; + var DEFAULT_ONEDRIVE_CLIENT_ID: string; + var DEFAULT_ONEDRIVE_AUTHORITY: string; +} + +export const DROPBOX_APP_KEY = global.DEFAULT_DROPBOX_APP_KEY; +export const ONEDRIVE_CLIENT_ID = global.DEFAULT_ONEDRIVE_CLIENT_ID; +export const ONEDRIVE_AUTHORITY = global.DEFAULT_ONEDRIVE_AUTHORITY; + export const DEFAULT_CONTENT_TYPE = "application/octet-stream"; export type SUPPORTED_SERVICES_TYPE = @@ -25,18 +36,13 @@ export type SUPPORTED_SERVICES_TYPE = | "box" | "pcloud" | "yandexdisk" - | "koofr"; + | "koofr" + | "azureblobstorage"; -export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR = - | "webdav" - | "dropbox" - | "onedrive" - | "webdis" - | "googledrive" - | "box" - | "pcloud" - | "yandexdisk" - | "koofr"; +export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR = Exclude< + SUPPORTED_SERVICES_TYPE, + "s3" | "azureblobstorage" +>; export interface S3Config { s3Endpoint: string; @@ -126,18 +132,7 @@ export type SyncDirectionType = export type CipherMethodType = "rclone-base64" | "openssl-base64" | "unknown"; -export type QRExportType = - | "basic_and_advanced" - | "s3" - | "dropbox" - | "onedrive" - | "webdav" - | "webdis" - | "googledrive" - | "box" - | "pcloud" - | "yandexdisk" - | "koofr"; +export type QRExportType = "basic_and_advanced" | SUPPORTED_SERVICES_TYPE; export interface ProfilerConfig { enablePrinting?: boolean; @@ -155,6 +150,7 @@ export interface RemotelySavePluginSettings { pcloud: PCloudConfig; yandexdisk: YandexDiskConfig; koofr: KoofrConfig; + azureblobstorage: AzureBlobStorageConfig; password: string; serviceType: SUPPORTED_SERVICES_TYPE; diff --git a/src/fsDropbox.ts b/src/fsDropbox.ts index b5c8cb4..eb1cb95 100644 --- a/src/fsDropbox.ts +++ b/src/fsDropbox.ts @@ -3,6 +3,7 @@ import type { DropboxResponse, DropboxResponseError, files } from "dropbox"; import random from "lodash/random"; import { COMMAND_CALLBACK_DROPBOX, + DROPBOX_APP_KEY, type DropboxConfig, type Entity, OAUTH2_FORCE_EXPIRE_MILLISECONDS, @@ -21,7 +22,7 @@ export { Dropbox } from "dropbox"; export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = { accessToken: "", - clientID: process.env.DEFAULT_DROPBOX_APP_KEY ?? "", + clientID: DROPBOX_APP_KEY ?? "", refreshToken: "", accessTokenExpiresInSeconds: 0, accessTokenExpiresAtTime: 0, diff --git a/src/fsGetter.ts b/src/fsGetter.ts index 15ad1fb..fd8f92d 100644 --- a/src/fsGetter.ts +++ b/src/fsGetter.ts @@ -1,3 +1,4 @@ +import { FakeFsAzureBlobStorage } from "../pro/src/fsAzureBlobStorage"; import { FakeFsBox } from "../pro/src/fsBox"; import { FakeFsGoogleDrive } from "../pro/src/fsGoogleDrive"; import { FakeFsKoofr } from "../pro/src/fsKoofr"; @@ -68,6 +69,8 @@ export function getClient( ); case "koofr": return new FakeFsKoofr(settings.koofr, vaultName, saveUpdatedConfigFunc); + case "azureblobstorage": + return new FakeFsAzureBlobStorage(settings.azureblobstorage, vaultName); default: throw Error(`cannot init client for serviceType=${settings.serviceType}`); } diff --git a/src/fsOnedrive.ts b/src/fsOnedrive.ts index ec852b2..37796bf 100644 --- a/src/fsOnedrive.ts +++ b/src/fsOnedrive.ts @@ -13,6 +13,8 @@ import { DEFAULT_CONTENT_TYPE, type Entity, OAUTH2_FORCE_EXPIRE_MILLISECONDS, + ONEDRIVE_AUTHORITY, + ONEDRIVE_CLIENT_ID, type OnedriveConfig, } from "./baseTypes"; import { VALID_REQURL } from "./baseTypesObs"; @@ -24,8 +26,8 @@ const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`; export const DEFAULT_ONEDRIVE_CONFIG: OnedriveConfig = { accessToken: "", - clientID: process.env.DEFAULT_ONEDRIVE_CLIENT_ID ?? "", - authority: process.env.DEFAULT_ONEDRIVE_AUTHORITY ?? "", + clientID: ONEDRIVE_CLIENT_ID ?? "", + authority: ONEDRIVE_AUTHORITY ?? "", refreshToken: "", accessTokenExpiresInSeconds: 0, accessTokenExpiresAtTime: 0, diff --git a/src/fsS3.ts b/src/fsS3.ts index cf96071..855f0cc 100644 --- a/src/fsS3.ts +++ b/src/fsS3.ts @@ -755,23 +755,44 @@ export class FakeFsS3 extends FakeFs { return; } - if (this.synthFoldersCache.hasOwnProperty(key)) { - delete this.synthFoldersCache[key]; - return; + if (key.endsWith("/")) { + if (this.synthFoldersCache.hasOwnProperty(key)) { + delete this.synthFoldersCache[key]; + return; + } + + // in s3 the folder may not exist, so we make our best effort. + // do NOT read this.s3Config.generateFolderObject + // because the folder might be generated by previous setting + try { + const remoteFileName = getRemoteWithPrefixPath( + key, + this.s3Config.remotePrefix ?? "" + ); + + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.s3Config.s3BucketName, + Key: remoteFileName, + }) + ); + } catch (e) { + // pass + } + } else { + const remoteFileName = getRemoteWithPrefixPath( + key, + this.s3Config.remotePrefix ?? "" + ); + + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.s3Config.s3BucketName, + Key: remoteFileName, + }) + ); } - const remoteFileName = getRemoteWithPrefixPath( - key, - this.s3Config.remotePrefix ?? "" - ); - - await this.s3Client.send( - new DeleteObjectCommand({ - Bucket: this.s3Config.s3BucketName, - Key: remoteFileName, - }) - ); - // TODO: do we need to delete folder recursively? // maybe we should not // because the outer sync algorithm should do that diff --git a/src/importExport.ts b/src/importExport.ts index a6aa4c3..fec81ce 100644 --- a/src/importExport.ts +++ b/src/importExport.ts @@ -29,6 +29,7 @@ export const exportQrCodeUri = async ( delete settings2.pcloud; delete settings2.yandexdisk; delete settings2.koofr; + delete settings2.azureblobstorage; delete settings2.pro; } else if (exportFields === "s3") { settings2 = { s3: cloneDeep(settings.s3) }; @@ -50,6 +51,8 @@ export const exportQrCodeUri = async ( settings2 = { yandexdisk: cloneDeep(settings.yandexdisk) }; } else if (exportFields === "koofr") { settings2 = { koofr: cloneDeep(settings.koofr) }; + } else if (exportFields === "azureblobstorage") { + settings2 = { azureblobstorage: cloneDeep(settings.azureblobstorage) }; } delete settings2.vaultRandomID; diff --git a/src/langs/en.json b/src/langs/en.json index 9f1de11..3a9bad1 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -82,13 +82,13 @@ "modal_remotebasedir_secondconfirm_vaultname": "Reset To The Default Vault Folder Name", "modal_remotebasedir_secondconfirm_change": "Confirm To Change", "modal_remotebasedir_notice": "New remote base directory config saved!", - "modal_remoteprefix_title": "You are changing the remote prefix config", - "modal_remoteprefix_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the prefix will be empty and the files will be saved at the root of the bucket.\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.", - "modal_remoteprefix_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.", - "modal_remoteprefix_tosave": "The prefix to save is \"{{{prefix}}}\"", - "modal_remoteprefix_secondconfirm_empty": "The prefix is empty and the files will be saved at the root of the bucket.", - "modal_remoteprefix_secondconfirm_change": "Confirm To Change", - "modal_remoteprefix_notice": "New remote prefix config saved!", + "modal_remoteprefix_s3_title": "You are changing the remote prefix config", + "modal_remoteprefix_s3_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the prefix will be empty and the files will be saved at the root of the bucket.\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.", + "modal_remoteprefix_s3_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.", + "modal_remoteprefix_s3_tosave": "The prefix to save is \"{{{prefix}}}\"", + "modal_remoteprefix_s3_secondconfirm_empty": "The prefix is empty and the files will be saved at the root of the bucket.", + "modal_remoteprefix_s3_secondconfirm_change": "Confirm To Change", + "modal_remoteprefix_s3_notice": "New remote prefix config saved!", "modal_dropboxauth_manualsteps": "Step 1: Visit the address in a browser, and follow the steps.\nStep 2: In the end of the web flow, you obtain a long code. Paste it here then click \"Submit\".", "modal_dropboxauth_autosteps": "Visit the address in a browser, and follow the steps.\nFinally you should be redirected to Obsidian.", "modal_dropboxauth_copybutton": "Click to copy the auth url", @@ -163,8 +163,8 @@ "settings_checkonnectivity_checking": "Checking...", "settings_remotebasedir": "Change The Remote Base Directory (experimental)", "settings_remotebasedir_desc": "By default the content is synced to a remote directory with the same name as the vault name. You can change the remote folder name here, or keep the input field empty to reset to the default. You need to click \"Confirm\".", - "settings_remoteprefix": "Change The Remote Prefix (experimental)", - "settings_remoteprefix_desc": "By default in s3 the files are saved at the root of the bucket. You can change the remote prefix here, or keep the input field empty to reset to the default. You need to click \"Confirm\".", + "settings_remoteprefix_s3": "Change The Remote Prefix (experimental)", + "settings_remoteprefix_s3_desc": "By default in s3 the files are saved at the root of the bucket. You can change the remote prefix here, or keep the input field empty to reset to the default. You need to click \"Confirm\".", "settings_s3": "Remote For S3 or compatible", "settings_s3_disclaimer1": "Disclaimer: This plugin is NOT an official Amazon product.", "settings_s3_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your bucket, please immediately delete the access key on your AWS (or other S3-service provider) settings.", diff --git a/src/langs/zh_cn.json b/src/langs/zh_cn.json index a2e1f78..4a569ba 100644 --- a/src/langs/zh_cn.json +++ b/src/langs/zh_cn.json @@ -82,13 +82,13 @@ "modal_remotebasedir_secondconfirm_vaultname": "重设回默认的库文件夹名", "modal_remotebasedir_secondconfirm_change": "确认修改", "modal_remotebasedir_notice": "新的远端基文件夹设置已保存!", - "modal_remoteprefix_title": "您正在修改远端路径前缀设置", - "modal_remoteprefix_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置为保存为空,文件将会被存储在桶(Bucket)的根目录。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。", - "modal_remoteprefix_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。", - "modal_remoteprefix_tosave": "您设定的新前缀为:“{{{prefix}}}”", - "modal_remoteprefix_secondconfirm_empty": "前缀为空,文件会保存在根目录", - "modal_remoteprefix_secondconfirm_change": "确认修改", - "modal_remoteprefix_notice": "新的远端路径前缀设置已保存!", + "modal_remoteprefix_s3_title": "您正在修改远端路径前缀设置", + "modal_remoteprefix_s3_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置为保存为空,文件将会被存储在桶(Bucket)的根目录。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。", + "modal_remoteprefix_s3_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。", + "modal_remoteprefix_s3_tosave": "您设定的新前缀为:“{{{prefix}}}”", + "modal_remoteprefix_s3_secondconfirm_empty": "前缀为空,文件会保存在根目录", + "modal_remoteprefix_s3_secondconfirm_change": "确认修改", + "modal_remoteprefix_s3_notice": "新的远端路径前缀设置已保存!", "modal_dropboxauth_manualsteps": "第 1 步:在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会获得一串很长的代码文本,请复制粘贴到下方,并点击“提交”", "modal_dropboxauth_autosteps": "在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会被自动重定向回来 Obsidian。", "modal_dropboxauth_copybutton": "点击此按钮从而复制鉴权 url", @@ -162,8 +162,8 @@ "settings_checkonnectivity_checking": "正在检查……", "settings_remotebasedir": "修改远端基文件夹(实验性质)", "settings_remotebasedir_desc": "默认设定,内容会被同步到远端的和资料库同名的文件夹下。您可以在此修改远端文件夹名,或删除输入框文本从而重设到默认值。您需要点击“确认”。", - "settings_remoteprefix": "修改远端前缀路径(实验性质)", - "settings_remoteprefix_desc": "默认设定 s3 保存在存储桶(Bucket)的根目录。您可以在这里修改路径前缀,或者保持为空保持默认设置。您需要点击“确认”。", + "settings_remoteprefix_s3": "修改远端前缀路径(实验性质)", + "settings_remoteprefix_s3_desc": "默认设定 s3 保存在存储桶(Bucket)的根目录。您可以在这里修改路径前缀,或者保持为空保持默认设置。您需要点击“确认”。", "settings_s3": "S3 或兼容 S3 的服务的设置", "settings_s3_disclaimer1": "声明:本插件不是 Amazon 的官方产品。", "settings_s3_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了存储桶有不符合预期的访问,请立刻从 AWS(或其它 S3 服务商)删除记录于此的 access key。", diff --git a/src/langs/zh_tw.json b/src/langs/zh_tw.json index 4e29002..1f28915 100644 --- a/src/langs/zh_tw.json +++ b/src/langs/zh_tw.json @@ -81,13 +81,13 @@ "modal_remotebasedir_secondconfirm_vaultname": "重設回預設的庫資料夾名", "modal_remotebasedir_secondconfirm_change": "確認修改", "modal_remotebasedir_notice": "新的遠端基資料夾設定已儲存!", - "modal_remoteprefix_title": "您正在修改遠端路徑字首設定", - "modal_remoteprefix_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定為儲存為空,檔案將會被儲存在桶(Bucket)的根目錄。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。", - "modal_remoteprefix_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。", - "modal_remoteprefix_tosave": "您設定的新字首為:“{{{prefix}}}”", - "modal_remoteprefix_secondconfirm_empty": "字首為空,檔案會儲存在根目錄", - "modal_remoteprefix_secondconfirm_change": "確認修改", - "modal_remoteprefix_notice": "新的遠端路徑字首設定已儲存!", + "modal_remoteprefix_s3_title": "您正在修改遠端路徑字首設定", + "modal_remoteprefix_s3_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定為儲存為空,檔案將會被儲存在桶(Bucket)的根目錄。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。", + "modal_remoteprefix_s3_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。", + "modal_remoteprefix_s3_tosave": "您設定的新字首為:“{{{prefix}}}”", + "modal_remoteprefix_s3_secondconfirm_empty": "字首為空,檔案會儲存在根目錄", + "modal_remoteprefix_s3_secondconfirm_change": "確認修改", + "modal_remoteprefix_s3_notice": "新的遠端路徑字首設定已儲存!", "modal_dropboxauth_manualsteps": "第 1 步:在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會獲得一串很長的程式碼文字,請複製貼上到下方,並點選“提交”", "modal_dropboxauth_autosteps": "在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會被自動重定向回來 Obsidian。", "modal_dropboxauth_copybutton": "點選此按鈕從而複製鑑權 url", @@ -161,8 +161,8 @@ "settings_checkonnectivity_checking": "正在檢查……", "settings_remotebasedir": "修改遠端基資料夾(實驗性質)", "settings_remotebasedir_desc": "預設設定,內容會被同步到遠端的和資料庫同名的資料夾下。您可以在此修改遠端資料夾名,或刪除輸入框文字從而重設到預設值。您需要點選“確認”。", - "settings_remoteprefix": "修改遠端字首路徑(實驗性質)", - "settings_remoteprefix_desc": "預設設定 s3 儲存在儲存桶(Bucket)的根目錄。您可以在這裡修改路徑字首,或者保持為空保持預設設定。您需要點選“確認”。", + "settings_remoteprefix_s3": "修改遠端字首路徑(實驗性質)", + "settings_remoteprefix_s3_desc": "預設設定 s3 儲存在儲存桶(Bucket)的根目錄。您可以在這裡修改路徑字首,或者保持為空保持預設設定。您需要點選“確認”。", "settings_s3": "S3 或相容 S3 的服務的設定", "settings_s3_disclaimer1": "宣告:本外掛不是 Amazon 的官方產品。", "settings_s3_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了儲存桶有不符合預期的訪問,請立刻從 AWS(或其它 S3 服務商)刪除記錄於此的 access key。", diff --git a/src/main.ts b/src/main.ts index a1e2841..f0b93ea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -30,6 +30,7 @@ import { COMMAND_CALLBACK_PRO, COMMAND_CALLBACK_YANDEXDISK, } from "../pro/src/baseTypesPro"; +import { DEFAULT_AZUREBLOBSTORAGE_CONFIG } from "../pro/src/fsAzureBlobStorage"; import { DEFAULT_BOX_CONFIG, FakeFsBox, @@ -115,6 +116,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = { pcloud: DEFAULT_PCLOUD_CONFIG, yandexdisk: DEFAULT_YANDEXDISK_CONFIG, koofr: DEFAULT_KOOFR_CONFIG, + azureblobstorage: DEFAULT_AZUREBLOBSTORAGE_CONFIG, password: "", serviceType: "s3", currLogLevel: "info", @@ -1376,6 +1378,10 @@ export default class RemotelySavePlugin extends Plugin { this.settings.koofr = DEFAULT_KOOFR_CONFIG; } + if (this.settings.azureblobstorage === undefined) { + this.settings.azureblobstorage = DEFAULT_AZUREBLOBSTORAGE_CONFIG; + } + await this.saveSettings(); } diff --git a/src/settings.ts b/src/settings.ts index 64011d1..801cd50 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -21,6 +21,7 @@ import type { } from "./baseTypes"; import cloneDeep from "lodash/cloneDeep"; +import { generateAzureBlobStorageSettingsPart } from "../pro/src/settingsAzureBlobStorage"; import { generateBoxSettingsPart } from "../pro/src/settingsBox"; import { generateGoogleDriveSettingsPart } from "../pro/src/settingsGoogleDrive"; import { generateKoofrSettingsPart } from "../pro/src/settingsKoofr"; @@ -290,7 +291,7 @@ export class ChangeRemoteBaseDirModal extends Modal { * s3 is special and do not necessarily the same as others * thus a new Modal here */ -class ChangeRemotePrefixModal extends Modal { +class ChangeS3RemotePrefixModal extends Modal { readonly plugin: RemotelySavePlugin; readonly newRemotePrefix: string; constructor(app: App, plugin: RemotelySavePlugin, newRemotePrefix: string) { @@ -306,8 +307,8 @@ class ChangeRemotePrefixModal extends Modal { return this.plugin.i18n.t(x, vars); }; - contentEl.createEl("h2", { text: t("modal_remoteprefix_title") }); - t("modal_remoteprefix_shortdesc") + contentEl.createEl("h2", { text: t("modal_remoteprefix_s3_title") }); + t("modal_remoteprefix_s3_shortdesc") .split("\n") .forEach((val, idx) => { contentEl.createEl("p", { @@ -316,7 +317,7 @@ class ChangeRemotePrefixModal extends Modal { }); contentEl.createEl("p", { - text: t("modal_remoteprefix_tosave", { prefix: this.newRemotePrefix }), + text: t("modal_remoteprefix_s3_tosave", { prefix: this.newRemotePrefix }), }); if ( @@ -325,12 +326,12 @@ class ChangeRemotePrefixModal extends Modal { ) { new Setting(contentEl) .addButton((button) => { - button.setButtonText(t("modal_remoteprefix_secondconfirm_empty")); + button.setButtonText(t("modal_remoteprefix_s3_secondconfirm_empty")); button.onClick(async () => { // in the settings, the value is reset to the special case "" this.plugin.settings.s3.remotePrefix = ""; await this.plugin.saveSettings(); - new Notice(t("modal_remoteprefix_notice")); + new Notice(t("modal_remoteprefix_s3_notice")); this.close(); }); button.setClass("remoteprefix-second-confirm"); @@ -344,14 +345,14 @@ class ChangeRemotePrefixModal extends Modal { } else { new Setting(contentEl) .addButton((button) => { - button.setButtonText(t("modal_remoteprefix_secondconfirm_change")); + button.setButtonText(t("modal_remoteprefix_s3_secondconfirm_change")); button.onClick(async () => { this.plugin.settings.s3.remotePrefix = this.newRemotePrefix; await this.plugin.saveSettings(); - new Notice(t("modal_remoteprefix_notice")); + new Notice(t("modal_remoteprefix_s3_notice")); this.close(); }); - button.setClass("remoteprefix-second-confirm"); + button.setClass("remoteprefix-s3-second-confirm"); }) .addButton((button) => { button.setButtonText(t("goback")); @@ -801,7 +802,7 @@ const getEyesElements = () => { }; }; -const wrapTextWithPasswordHide = (text: TextComponent) => { +export const wrapTextWithPasswordHide = (text: TextComponent) => { const { eye, eyeOff } = getEyesElements(); const hider = text.inputEl.insertAdjacentElement("afterend", createSpan())!; // the init type of hider is "hidden" === eyeOff === password @@ -1054,8 +1055,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab { let newS3RemotePrefix = this.plugin.settings.s3.remotePrefix || ""; new Setting(s3Div) - .setName(t("settings_remoteprefix")) - .setDesc(t("settings_remoteprefix_desc")) + .setName(t("settings_remoteprefix_s3")) + .setDesc(t("settings_remoteprefix_s3_desc")) .addText((text) => text .setPlaceholder("") @@ -1067,7 +1068,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab { .addButton((button) => { button.setButtonText(t("confirm")); button.onClick(() => { - new ChangeRemotePrefixModal( + new ChangeS3RemotePrefixModal( this.app, this.plugin, simpleTransRemotePrefix(newS3RemotePrefix.trim()) @@ -1873,6 +1874,22 @@ export class RemotelySaveSettingTab extends PluginSettingTab { this.plugin.saveSettings() ); + ////////////////////////////////////////////////// + // below for Azure Blob Storage + ////////////////////////////////////////////////// + + const { + azureBlobStorageDiv, + azureBlobStorageAllowedToUsedDiv, + azureBlobStorageNotShowUpHintSetting, + } = generateAzureBlobStorageSettingsPart( + containerEl, + t, + this.app, + this.plugin, + () => this.plugin.saveSettings() + ); + ////////////////////////////////////////////////// // below for general chooser (part 2/2) ////////////////////////////////////////////////// @@ -1899,6 +1916,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab { t("settings_chooseservice_yandexdisk") ); dropdown.addOption("koofr", t("settings_chooseservice_koofr")); + dropdown.addOption( + "azureblobstorage", + t("settings_chooseservice_azureblobstorage") + ); dropdown .setValue(this.plugin.settings.serviceType) @@ -1944,6 +1965,11 @@ export class RemotelySaveSettingTab extends PluginSettingTab { "koofr-hide", this.plugin.settings.serviceType !== "koofr" ); + azureBlobStorageDiv.toggleClass( + "azureblobstorage-hide", + this.plugin.settings.serviceType !== "azureblobstorage" + ); + await this.plugin.saveSettings(); }); }); @@ -2542,6 +2568,16 @@ export class RemotelySaveSettingTab extends PluginSettingTab { button.onClick(async () => { new ExportSettingsQrCodeModal(this.app, this.plugin, "koofr").open(); }); + }) + .addButton(async (button) => { + button.setButtonText(t("settings_export_azureblobstorage_button")); + button.onClick(async () => { + new ExportSettingsQrCodeModal( + this.app, + this.plugin, + "azureblobstorage" + ).open(); + }); }); let importSettingVal = ""; @@ -2616,7 +2652,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab { yandexDiskAllowedToUsedDiv, yandexDiskNotShowUpHintSetting, koofrAllowedToUsedDiv, - koofrNotShowUpHintSetting + koofrNotShowUpHintSetting, + azureBlobStorageAllowedToUsedDiv, + azureBlobStorageNotShowUpHintSetting ); ////////////////////////////////////////////////// diff --git a/styles.css b/styles.css index f5f23d3..11ec1d2 100644 --- a/styles.css +++ b/styles.css @@ -171,6 +171,17 @@ display: none; } +.azureblobstorage-disclaimer { + font-weight: bold; +} +.azureblobstorage-hide { + display: none; +} + +.azureblobstorage-allow-to-use-hide { + display: none; +} + .qrcode-img { width: 350px; height: 350px; diff --git a/tests/configPersist.test.ts b/tests/configPersist.test.ts index a357fc7..7a9b157 100644 --- a/tests/configPersist.test.ts +++ b/tests/configPersist.test.ts @@ -34,6 +34,9 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = { koofr: { refreshToken: "xxx", } as any, + azureblobstorage: { + containerSasUrl: "http://127.0.0.1", + } as any, password: "password", serviceType: "s3", currLogLevel: "info", diff --git a/tsconfig.json b/tsconfig.json index 238e2c4..91ed71e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,5 @@ "isolatedModules": true, "lib": ["dom", "es5", "scripthost", "es2015", "webworker"] }, - "include": ["**/*.ts"] + "include": ["src/global.d.ts", "**/*.ts"] } diff --git a/webpack.config.js b/webpack.config.js index 28267c6..c45de8b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -31,30 +31,37 @@ module.exports = { }, plugins: [ new webpack.DefinePlugin({ - "process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`, - "process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`, - "process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`, - "process.env.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`, - "process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`, - "process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`, - "process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`, - "process.env.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`, - "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}"`, - "process.env.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`, - "process.env.DEFAULT_KOOFR_CLIENT_SECRET": `"${DEFAULT_KOOFR_CLIENT_SECRET}"`, + "global.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`, + "global.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`, + "global.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`, + "global.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`, + "global.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`, + "global.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`, + "global.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`, + "global.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`, + "global.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`, + "global.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`, + "global.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`, + "global.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`, + "global.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`, + "global.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`, + "global.DEFAULT_KOOFR_CLIENT_SECRET": `"${DEFAULT_KOOFR_CLIENT_SECRET}"`, + + "process.env.NODE_DEBUG": `undefined`, // ugly fix + "process.env.DEBUG": `undefined`, // ugly fix + // "process.version": `"v20.10.0"`, // who's using this? + // "process":`undefined`, + // "global.process":`undefined`, + "globalThis.process": `undefined`, // make azure blob storage happy }), // Work around for Buffer is undefined: // https://github.com/webpack/changelog-v5/issues/10 new webpack.ProvidePlugin({ Buffer: ["buffer", "Buffer"], }), - new webpack.ProvidePlugin({ - process: "process/browser", - }), + // new webpack.ProvidePlugin({ + // process: "process/browser", + // }), ], module: { rules: [ @@ -103,7 +110,7 @@ module.exports = { // os: require.resolve("os-browserify/browser"), path: require.resolve("path-browserify"), // punycode: require.resolve("punycode"), - process: require.resolve("process/browser"), + process: false, // require.resolve("process/browser"), // querystring: require.resolve("querystring-es3"), stream: require.resolve("stream-browserify"), // string_decoder: require.resolve("string_decoder"),