This commit is contained in:
fyears 2024-06-22 21:03:43 +08:00
parent 690f56945a
commit cc0a286471
37 changed files with 1165 additions and 140 deletions

View File

@ -27,6 +27,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
- pCloud (PRO feature) - pCloud (PRO feature)
- Yandex Disk (PRO feature) - Yandex Disk (PRO feature)
- Koofr (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. - [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". - **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. - **[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). 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) ## 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. Basic (free) version can detect conflicts, but users have to choose to keep newer version or larger version of the files.

View File

@ -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://<account>.blob.core.windows.net/<container name>?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 `<your vault name>/`. 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.**

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:83caba9a42772f995035e3c879c6119eabcd6d3c27298cf0fb067b3676680571
size 107057

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef2d525d3ac7ecca26f7f8b6782fb1056d54f8c558a8d77586571defb7e6b5be
size 41282

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cba5bf27e76eaf8ea21e51e43172501fc4867539c90c1758a02945477dc261ea
size 132487

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b2add8a559eab0b7073f7ed52d4a3e4efae68b499c7b924d1ac50278758961b
size 193894

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:582a69a7aee2cfab72dc14324e4a864acab92c145d533e799f8ceea61a8f0102
size 478941

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cbb8863d98d8826739b5bf9fa4c7a2de3ba49fde4bbe3c4988a877ba74f89abd
size 143007

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ddc30f97d75d7c3a6061cbb30cc991cbdf1f4039aad3547ef88998c3dd9be666
size 132904

View File

@ -17,6 +17,7 @@ The list is for information purposes only.
| [filebase](https://filebase.com/) | Yes | Yes | | | | [filebase](https://filebase.com/) | Yes | Yes | | |
| QingStor 青云 | ? | ? | | | | QingStor 青云 | ? | ? | | |
| [MinIO](https://min.io/) | Yes | Yes | | | | [MinIO](https://min.io/) | Yes | Yes | | |
| Azure Blob Storage | Yes (PRO) | | | Yes (PRO) |
| [WsgiDAV](https://github.com/mar10/wsgidav) | Yes | | Yes | | | [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 | | | [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes | | Yes | |
| NextCloud | Yes | | Yes | | | NextCloud | Yes | | Yes | |

View File

@ -51,6 +51,7 @@ esbuild
"http", "http",
"https", "https",
"vm", "vm",
// "process",
// ...builtins // ...builtins
], ],
inject: ["./esbuild.injecthelper.mjs"], inject: ["./esbuild.injecthelper.mjs"],
@ -63,24 +64,28 @@ esbuild
minify: prod, minify: prod,
outfile: "main.js", outfile: "main.js",
define: { define: {
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`, "global.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`, "global.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
"process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`, "global.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`,
"process.env.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`, "global.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`,
"process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`, "global.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`,
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`, "global.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`,
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`, "global.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`,
"process.env.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`, "global.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`,
"process.env.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`, "global.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`,
"process.env.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`, "global.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`,
"process.env.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`, "global.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`,
"process.env.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`, "global.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`,
"process.env.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`, "global.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`,
"process.env.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`, "global.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`,
"process.env.DEFAULT_KOOFR_CLIENT_SECRET": `"${DEFAULT_KOOFR_CLIENT_SECRET}"`, "global.DEFAULT_KOOFR_CLIENT_SECRET": `"${DEFAULT_KOOFR_CLIENT_SECRET}"`,
global: "window", global: "window",
"process.env.NODE_DEBUG": `undefined`, // ugly fix "process.env.NODE_DEBUG": `undefined`, // ugly fix
"process.env.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()], plugins: [inlineWorkerPlugin()],
}) })

View File

@ -1,2 +1,2 @@
export const Buffer = require("buffer").Buffer; export const Buffer = require("buffer").Buffer;
export const process = require("process/browser"); // export const process = require("process/browser");

View File

@ -13,7 +13,7 @@
}, },
"browser": { "browser": {
"path": "path-browserify", "path": "path-browserify",
"process": "process/browser", "process": false,
"stream": "stream-browserify", "stream": "stream-browserify",
"crypto": "crypto-browserify", "crypto": "crypto-browserify",
"url": "url/", "url": "url/",
@ -61,6 +61,7 @@
"@aws-sdk/signature-v4-crt": "^3.556.0", "@aws-sdk/signature-v4-crt": "^3.556.0",
"@aws-sdk/types": "^3.535.0", "@aws-sdk/types": "^3.535.0",
"@azure/msal-node": "^2.7.0", "@azure/msal-node": "^2.7.0",
"@azure/storage-blob": "^12.23.0",
"@fyears/rclone-crypt": "^0.0.7", "@fyears/rclone-crypt": "^0.0.7",
"@fyears/tsqueue": "^1.0.1", "@fyears/tsqueue": "^1.0.1",
"@microsoft/microsoft-graph-client": "^3.0.7", "@microsoft/microsoft-graph-client": "^3.0.7",

View File

@ -290,6 +290,11 @@ export const checkProRunnableAndFixInplace = async (
service: "koofr", service: "koofr",
name: "Koofr", name: "Koofr",
}, },
{
feature: "feature-azure_blob_storage",
service: "azureblobstorage",
name: "Azure Blob Storage",
},
]; ];
for (const { feature, service, name } of toChecked) { for (const { feature, service, name } of toChecked) {

View File

@ -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 // PRO
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
export const COMMAND_CALLBACK_PRO = "remotely-save-cb-pro"; export const COMMAND_CALLBACK_PRO = "remotely-save-cb-pro";
export const PRO_CLIENT_ID = process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID; export const PRO_CLIENT_ID = global.DEFAULT_REMOTELYSAVE_CLIENT_ID;
export const PRO_WEBSITE = process.env.DEFAULT_REMOTELYSAVE_WEBSITE; export const PRO_WEBSITE = global.DEFAULT_REMOTELYSAVE_WEBSITE;
export type PRO_FEATURE_TYPE = export type PRO_FEATURE_TYPE =
| "feature-smart_conflict" | "feature-smart_conflict"
@ -12,7 +31,8 @@ export type PRO_FEATURE_TYPE =
| "feature-box" | "feature-box"
| "feature-pcloud" | "feature-pcloud"
| "feature-yandex_disk" | "feature-yandex_disk"
| "feature-koofr"; | "feature-koofr"
| "feature-azure_blob_storage";
export interface FeatureInfo { export interface FeatureInfo {
featureName: PRO_FEATURE_TYPE; featureName: PRO_FEATURE_TYPE;
@ -51,18 +71,17 @@ export interface GoogleDriveConfig {
kind: "googledrive"; kind: "googledrive";
} }
export const DEFAULT_GOOGLEDRIVE_CLIENT_ID = export const GOOGLEDRIVE_CLIENT_ID = global.DEFAULT_GOOGLEDRIVE_CLIENT_ID;
process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID; export const GOOGLEDRIVE_CLIENT_SECRET =
export const DEFAULT_GOOGLEDRIVE_CLIENT_SECRET = global.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET;
process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET;
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////
// box // box
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
export const COMMAND_CALLBACK_BOX = "remotely-save-cb-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_ID = global.DEFAULT_BOX_CLIENT_ID;
export const BOX_CLIENT_SECRET = process.env.DEFAULT_BOX_CLIENT_SECRET; export const BOX_CLIENT_SECRET = global.DEFAULT_BOX_CLIENT_SECRET;
export interface BoxConfig { export interface BoxConfig {
accessToken: string; accessToken: string;
@ -79,8 +98,8 @@ export interface BoxConfig {
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
export const COMMAND_CALLBACK_PCLOUD = "remotely-save-cb-pcloud"; export const COMMAND_CALLBACK_PCLOUD = "remotely-save-cb-pcloud";
export const PCLOUD_CLIENT_ID = process.env.DEFAULT_PCLOUD_CLIENT_ID; export const PCLOUD_CLIENT_ID = global.DEFAULT_PCLOUD_CLIENT_ID;
export const PCLOUD_CLIENT_SECRET = process.env.DEFAULT_PCLOUD_CLIENT_SECRET; export const PCLOUD_CLIENT_SECRET = global.DEFAULT_PCLOUD_CLIENT_SECRET;
export interface PCloudConfig { export interface PCloudConfig {
accessToken: string; accessToken: string;
@ -101,9 +120,8 @@ export interface PCloudConfig {
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
export const COMMAND_CALLBACK_YANDEXDISK = "remotely-save-cb-yandexdisk"; export const COMMAND_CALLBACK_YANDEXDISK = "remotely-save-cb-yandexdisk";
export const YANDEXDISK_CLIENT_ID = process.env.DEFAULT_YANDEXDISK_CLIENT_ID; export const YANDEXDISK_CLIENT_ID = global.DEFAULT_YANDEXDISK_CLIENT_ID;
export const YANDEXDISK_CLIENT_SECRET = export const YANDEXDISK_CLIENT_SECRET = global.DEFAULT_YANDEXDISK_CLIENT_SECRET;
process.env.DEFAULT_YANDEXDISK_CLIENT_SECRET;
export interface YandexDiskConfig { export interface YandexDiskConfig {
accessToken: string; accessToken: string;
@ -121,8 +139,8 @@ export interface YandexDiskConfig {
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
export const COMMAND_CALLBACK_KOOFR = "remotely-save-cb-koofr"; export const COMMAND_CALLBACK_KOOFR = "remotely-save-cb-koofr";
export const KOOFR_CLIENT_ID = process.env.DEFAULT_KOOFR_CLIENT_ID; export const KOOFR_CLIENT_ID = global.DEFAULT_KOOFR_CLIENT_ID;
export const KOOFR_CLIENT_SECRET = process.env.DEFAULT_KOOFR_CLIENT_SECRET; export const KOOFR_CLIENT_SECRET = global.DEFAULT_KOOFR_CLIENT_SECRET;
export interface KoofrConfig { export interface KoofrConfig {
accessToken: string; accessToken: string;
@ -136,3 +154,16 @@ export interface KoofrConfig {
mountID: string; mountID: string;
kind: "koofr"; kind: "koofr";
} }
///////////////////////////////////////////////////////////
// Azure Blob Storage
//////////////////////////////////////////////////////////
export interface AzureBlobStorageConfig {
containerSasUrl: string;
containerName: string;
remotePrefix: string;
generateFolderObject: boolean;
partsConcurrency: number;
kind: "azureblobstorage";
}

View File

@ -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<string, Entity>;
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<Entity[]> {
const entities: Entity[] = [];
const realEntities = new Set<string>();
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<Entity[]> {
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<Entity> {
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<Entity> {
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<Entity> {
const blobPath = getBlobPath(key, this.remotePrefix);
const blobClient = this.containerClient.getBlockBlobClient(blobPath);
const metadata: Record<string, string> = {
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<ArrayBuffer> {
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<void> {
throw new Error("Method not implemented.");
}
async rm(key: string): Promise<void> {
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<boolean> {
// 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<string> {
throw new Error("Method not implemented.");
}
async revokeAuth(): Promise<any> {
throw new Error("Method not implemented.");
}
allowEmptyFile(): boolean {
return true;
}
}

View File

@ -14,8 +14,8 @@ import {
unixTimeToStr, unixTimeToStr,
} from "../../src/misc"; } from "../../src/misc";
import { import {
DEFAULT_GOOGLEDRIVE_CLIENT_ID, GOOGLEDRIVE_CLIENT_ID,
DEFAULT_GOOGLEDRIVE_CLIENT_SECRET, GOOGLEDRIVE_CLIENT_SECRET,
type GoogleDriveConfig, type GoogleDriveConfig,
} from "./baseTypesPro"; } from "./baseTypesPro";
@ -103,8 +103,8 @@ export const sendRefreshTokenReq = async (refreshToken: string) => {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
body: new URLSearchParams({ body: new URLSearchParams({
client_id: DEFAULT_GOOGLEDRIVE_CLIENT_ID ?? "", client_id: GOOGLEDRIVE_CLIENT_ID ?? "",
client_secret: DEFAULT_GOOGLEDRIVE_CLIENT_SECRET ?? "", client_secret: GOOGLEDRIVE_CLIENT_SECRET ?? "",
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: refreshToken, refresh_token: refreshToken,
}).toString(), }).toString(),

View File

@ -92,6 +92,13 @@
"modal_koofrrevokeauth_clean_notice": "Cleaned!", "modal_koofrrevokeauth_clean_notice": "Cleaned!",
"modal_koofrrevokeauth_clean_fail": "Something goes wrong while revoking.", "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": "Revoke auth by clicking here and follow the steps.",
"modal_prorevokeauth_clean": "Clean", "modal_prorevokeauth_clean": "Clean",
"modal_prorevokeauth_clean_desc": "Clean local auth record", "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_succ": "Great! We can connect to Koofr!",
"settings_koofr_connect_fail": "We cannot 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 <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/remote_services/azureblobstorage/README.md\">try to change your policy</a> or account info.",
"settings_azureblobstorage_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_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 <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/remote_services/azureblobstorage/README.md\">doc</a> 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 <strong>vault name</strong> will be used as prefix. <strong>Empty prefix is not allowed and not settable here.</strong>",
"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_googledrive_button": "Export Google Drive Part",
"settings_export_box_button": "Export Box Part", "settings_export_box_button": "Export Box Part",
"settings_export_pcloud_button": "Export pCloud Part", "settings_export_pcloud_button": "Export pCloud Part",
"settings_export_yandexdisk_button": "Export Yandex Disk Part", "settings_export_yandexdisk_button": "Export Yandex Disk Part",
"settings_export_koofr_button": "Export Koofr 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": "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.", "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.",

View File

@ -102,6 +102,14 @@
"modal_koofrrevokeauth_clean_notice": "已清理!", "modal_koofrrevokeauth_clean_notice": "已清理!",
"modal_koofrrevokeauth_clean_fail": "清理授权时候发生了错误。", "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": "点击这里和按照步骤取消授权。",
"modal_prorevokeauth_clean": "清理", "modal_prorevokeauth_clean": "清理",
"modal_prorevokeauth_clean_desc": "清理本地授权记录", "modal_prorevokeauth_clean_desc": "清理本地授权记录",
@ -209,11 +217,36 @@
"settings_koofr_connect_succ": "很好!我们可连接上 Koofr", "settings_koofr_connect_succ": "很好!我们可连接上 Koofr",
"settings_koofr_connect_fail": "我们未能连接上 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 然后<a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/remote_services/azureblobstorage/README.md\">修改预设的 policy</a> 或账号信息。",
"settings_azureblobstorage_pro_desc": "<p><strong>!!这是 PRO付费功能! 您需要在线账号来使用此功能!!</strong><a href=\"#settings-pro\">向下滑</a>可以看到 PRO 账号的更多信息。)</p>",
"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。可阅读<a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/remote_services/azureblobstorage/README.md\">文档</a>获取更多说明。",
"settings_azureblobstorage_containername": "Container 名字",
"settings_azureblobstorage_containername_desc": "输入 Container 名字。",
"settings_azureblobstorage_remoteprefix": "修改远端前缀路径",
"settings_azureblobstorage_remoteprefix_desc": "您可以在这里修改路径前缀。如果设置为空,那么<strong>库vault名字</strong>会作为前缀。<strong>不允许设置空前缀。</strong>您需要点击“确认”。",
"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_googledrive_button": "导出 Google Drive 部分",
"settings_export_box_button": "导出 Box 部分", "settings_export_box_button": "导出 Box 部分",
"settings_export_pcloud_button": "导出 pCloud 部分", "settings_export_pcloud_button": "导出 pCloud 部分",
"settings_export_yandexdisk_button": "导出 Yandex Disk 部分", "settings_export_yandexdisk_button": "导出 Yandex Disk 部分",
"settings_export_koofr_button": "导出 Koofr 部分", "settings_export_koofr_button": "导出 Koofr 部分",
"settings_export_azureblobstorage_button": "导出 Azure Blob Storage 部分",
"settings_pro": "账号PRO 付费功能)", "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>第二部:点击“连接”按钮,从而连接本设备和在线账号。", "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>第二部:点击“连接”按钮,从而连接本设备和在线账号。",

View File

@ -102,6 +102,14 @@
"modal_koofrrevokeauth_clean_notice": "已清理!", "modal_koofrrevokeauth_clean_notice": "已清理!",
"modal_koofrrevokeauth_clean_fail": "清理授權時候發生了錯誤。", "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": "點選這裡和按照步驟取消授權。",
"modal_prorevokeauth_clean": "清理", "modal_prorevokeauth_clean": "清理",
"modal_prorevokeauth_clean_desc": "清理本地授權記錄", "modal_prorevokeauth_clean_desc": "清理本地授權記錄",
@ -209,11 +217,36 @@
"settings_koofr_connect_succ": "很好!我們可連線上 Koofr", "settings_koofr_connect_succ": "很好!我們可連線上 Koofr",
"settings_koofr_connect_fail": "我們未能連線上 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 然後<a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/remote_services/azureblobstorage/README.md\">修改預設的 policy</a> 或賬號資訊。",
"settings_azureblobstorage_pro_desc": "<p><strong>!!這是 PRO付費功能! 您需要線上賬號來使用此功能!!</strong><a href=\"#settings-pro\">向下滑</a>可以看到 PRO 賬號的更多資訊。)</p>",
"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。可閱讀<a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/remote_services/azureblobstorage/README.md\">文件</a>獲取更多說明。",
"settings_azureblobstorage_containername": "Container 名字",
"settings_azureblobstorage_containername_desc": "輸入 Container 名字。",
"settings_azureblobstorage_remoteprefix": "修改遠端字首路徑",
"settings_azureblobstorage_remoteprefix_desc": "您可以在這裡修改路徑字首。如果設定為空,那麼<strong>庫vault名字</strong>會作為字首。<strong>不允許設定空字首。</strong>您需要點選“確認”。",
"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_googledrive_button": "匯出 Google Drive 部分",
"settings_export_box_button": "匯出 Box 部分", "settings_export_box_button": "匯出 Box 部分",
"settings_export_pcloud_button": "匯出 pCloud 部分", "settings_export_pcloud_button": "匯出 pCloud 部分",
"settings_export_yandexdisk_button": "匯出 Yandex Disk 部分", "settings_export_yandexdisk_button": "匯出 Yandex Disk 部分",
"settings_export_koofr_button": "匯出 Koofr 部分", "settings_export_koofr_button": "匯出 Koofr 部分",
"settings_export_azureblobstorage_button": "匯出 Azure Blob Storage 部分",
"settings_pro": "賬號PRO 付費功能)", "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>第二部:點選“連線”按鈕,從而連線本裝置和線上賬號。", "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>第二部:點選“連線”按鈕,從而連線本裝置和線上賬號。",

View File

@ -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<any> | 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,
};
};

View File

@ -257,7 +257,9 @@ export const generateProSettingsPart = (
yandexDiskAllowedToUsedDiv: HTMLDivElement, yandexDiskAllowedToUsedDiv: HTMLDivElement,
yandexDiskNotShowUpHintSetting: Setting, yandexDiskNotShowUpHintSetting: Setting,
koofrAllowedToUsedDiv: HTMLDivElement, koofrAllowedToUsedDiv: HTMLDivElement,
koofrNotShowUpHintSetting: Setting koofrNotShowUpHintSetting: Setting,
azureBlobStorageAllowedToUsedDiv: HTMLDivElement,
azureBlobStorageNotShowUpHintSetting: Setting
) => { ) => {
proDiv proDiv
.createEl("h2", { text: t("settings_pro") }) .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")); new Notice(t("settings_pro_features_refresh_succ"));
}); });
}); });

View File

@ -4,6 +4,7 @@
*/ */
import type { import type {
AzureBlobStorageConfig,
BoxConfig, BoxConfig,
GoogleDriveConfig, GoogleDriveConfig,
KoofrConfig, KoofrConfig,
@ -13,6 +14,16 @@ import type {
} from "../pro/src/baseTypesPro"; } from "../pro/src/baseTypesPro";
import type { LangTypeAndAuto } from "./i18n"; 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 const DEFAULT_CONTENT_TYPE = "application/octet-stream";
export type SUPPORTED_SERVICES_TYPE = export type SUPPORTED_SERVICES_TYPE =
@ -25,18 +36,13 @@ export type SUPPORTED_SERVICES_TYPE =
| "box" | "box"
| "pcloud" | "pcloud"
| "yandexdisk" | "yandexdisk"
| "koofr"; | "koofr"
| "azureblobstorage";
export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR = export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR = Exclude<
| "webdav" SUPPORTED_SERVICES_TYPE,
| "dropbox" "s3" | "azureblobstorage"
| "onedrive" >;
| "webdis"
| "googledrive"
| "box"
| "pcloud"
| "yandexdisk"
| "koofr";
export interface S3Config { export interface S3Config {
s3Endpoint: string; s3Endpoint: string;
@ -126,18 +132,7 @@ export type SyncDirectionType =
export type CipherMethodType = "rclone-base64" | "openssl-base64" | "unknown"; export type CipherMethodType = "rclone-base64" | "openssl-base64" | "unknown";
export type QRExportType = export type QRExportType = "basic_and_advanced" | SUPPORTED_SERVICES_TYPE;
| "basic_and_advanced"
| "s3"
| "dropbox"
| "onedrive"
| "webdav"
| "webdis"
| "googledrive"
| "box"
| "pcloud"
| "yandexdisk"
| "koofr";
export interface ProfilerConfig { export interface ProfilerConfig {
enablePrinting?: boolean; enablePrinting?: boolean;
@ -155,6 +150,7 @@ export interface RemotelySavePluginSettings {
pcloud: PCloudConfig; pcloud: PCloudConfig;
yandexdisk: YandexDiskConfig; yandexdisk: YandexDiskConfig;
koofr: KoofrConfig; koofr: KoofrConfig;
azureblobstorage: AzureBlobStorageConfig;
password: string; password: string;
serviceType: SUPPORTED_SERVICES_TYPE; serviceType: SUPPORTED_SERVICES_TYPE;

View File

@ -3,6 +3,7 @@ import type { DropboxResponse, DropboxResponseError, files } from "dropbox";
import random from "lodash/random"; import random from "lodash/random";
import { import {
COMMAND_CALLBACK_DROPBOX, COMMAND_CALLBACK_DROPBOX,
DROPBOX_APP_KEY,
type DropboxConfig, type DropboxConfig,
type Entity, type Entity,
OAUTH2_FORCE_EXPIRE_MILLISECONDS, OAUTH2_FORCE_EXPIRE_MILLISECONDS,
@ -21,7 +22,7 @@ export { Dropbox } from "dropbox";
export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = { export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = {
accessToken: "", accessToken: "",
clientID: process.env.DEFAULT_DROPBOX_APP_KEY ?? "", clientID: DROPBOX_APP_KEY ?? "",
refreshToken: "", refreshToken: "",
accessTokenExpiresInSeconds: 0, accessTokenExpiresInSeconds: 0,
accessTokenExpiresAtTime: 0, accessTokenExpiresAtTime: 0,

View File

@ -1,3 +1,4 @@
import { FakeFsAzureBlobStorage } from "../pro/src/fsAzureBlobStorage";
import { FakeFsBox } from "../pro/src/fsBox"; import { FakeFsBox } from "../pro/src/fsBox";
import { FakeFsGoogleDrive } from "../pro/src/fsGoogleDrive"; import { FakeFsGoogleDrive } from "../pro/src/fsGoogleDrive";
import { FakeFsKoofr } from "../pro/src/fsKoofr"; import { FakeFsKoofr } from "../pro/src/fsKoofr";
@ -68,6 +69,8 @@ export function getClient(
); );
case "koofr": case "koofr":
return new FakeFsKoofr(settings.koofr, vaultName, saveUpdatedConfigFunc); return new FakeFsKoofr(settings.koofr, vaultName, saveUpdatedConfigFunc);
case "azureblobstorage":
return new FakeFsAzureBlobStorage(settings.azureblobstorage, vaultName);
default: default:
throw Error(`cannot init client for serviceType=${settings.serviceType}`); throw Error(`cannot init client for serviceType=${settings.serviceType}`);
} }

View File

@ -13,6 +13,8 @@ import {
DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE,
type Entity, type Entity,
OAUTH2_FORCE_EXPIRE_MILLISECONDS, OAUTH2_FORCE_EXPIRE_MILLISECONDS,
ONEDRIVE_AUTHORITY,
ONEDRIVE_CLIENT_ID,
type OnedriveConfig, type OnedriveConfig,
} from "./baseTypes"; } from "./baseTypes";
import { VALID_REQURL } from "./baseTypesObs"; import { VALID_REQURL } from "./baseTypesObs";
@ -24,8 +26,8 @@ const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
export const DEFAULT_ONEDRIVE_CONFIG: OnedriveConfig = { export const DEFAULT_ONEDRIVE_CONFIG: OnedriveConfig = {
accessToken: "", accessToken: "",
clientID: process.env.DEFAULT_ONEDRIVE_CLIENT_ID ?? "", clientID: ONEDRIVE_CLIENT_ID ?? "",
authority: process.env.DEFAULT_ONEDRIVE_AUTHORITY ?? "", authority: ONEDRIVE_AUTHORITY ?? "",
refreshToken: "", refreshToken: "",
accessTokenExpiresInSeconds: 0, accessTokenExpiresInSeconds: 0,
accessTokenExpiresAtTime: 0, accessTokenExpiresAtTime: 0,

View File

@ -755,23 +755,44 @@ export class FakeFsS3 extends FakeFs {
return; return;
} }
if (this.synthFoldersCache.hasOwnProperty(key)) { if (key.endsWith("/")) {
delete this.synthFoldersCache[key]; if (this.synthFoldersCache.hasOwnProperty(key)) {
return; 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? // TODO: do we need to delete folder recursively?
// maybe we should not // maybe we should not
// because the outer sync algorithm should do that // because the outer sync algorithm should do that

View File

@ -29,6 +29,7 @@ export const exportQrCodeUri = async (
delete settings2.pcloud; delete settings2.pcloud;
delete settings2.yandexdisk; delete settings2.yandexdisk;
delete settings2.koofr; delete settings2.koofr;
delete settings2.azureblobstorage;
delete settings2.pro; delete settings2.pro;
} else if (exportFields === "s3") { } else if (exportFields === "s3") {
settings2 = { s3: cloneDeep(settings.s3) }; settings2 = { s3: cloneDeep(settings.s3) };
@ -50,6 +51,8 @@ export const exportQrCodeUri = async (
settings2 = { yandexdisk: cloneDeep(settings.yandexdisk) }; settings2 = { yandexdisk: cloneDeep(settings.yandexdisk) };
} else if (exportFields === "koofr") { } else if (exportFields === "koofr") {
settings2 = { koofr: cloneDeep(settings.koofr) }; settings2 = { koofr: cloneDeep(settings.koofr) };
} else if (exportFields === "azureblobstorage") {
settings2 = { azureblobstorage: cloneDeep(settings.azureblobstorage) };
} }
delete settings2.vaultRandomID; delete settings2.vaultRandomID;

View File

@ -82,13 +82,13 @@
"modal_remotebasedir_secondconfirm_vaultname": "Reset To The Default Vault Folder Name", "modal_remotebasedir_secondconfirm_vaultname": "Reset To The Default Vault Folder Name",
"modal_remotebasedir_secondconfirm_change": "Confirm To Change", "modal_remotebasedir_secondconfirm_change": "Confirm To Change",
"modal_remotebasedir_notice": "New remote base directory config saved!", "modal_remotebasedir_notice": "New remote base directory config saved!",
"modal_remoteprefix_title": "You are changing the remote prefix config", "modal_remoteprefix_s3_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_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_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.", "modal_remoteprefix_s3_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.",
"modal_remoteprefix_tosave": "The prefix to save is \"{{{prefix}}}\"", "modal_remoteprefix_s3_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_s3_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_s3_secondconfirm_change": "Confirm To Change",
"modal_remoteprefix_notice": "New remote prefix config saved!", "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_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_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", "modal_dropboxauth_copybutton": "Click to copy the auth url",
@ -163,8 +163,8 @@
"settings_checkonnectivity_checking": "Checking...", "settings_checkonnectivity_checking": "Checking...",
"settings_remotebasedir": "Change The Remote Base Directory (experimental)", "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_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_s3": "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_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": "Remote For S3 or compatible",
"settings_s3_disclaimer1": "Disclaimer: This plugin is NOT an official Amazon product.", "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.", "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.",

View File

@ -82,13 +82,13 @@
"modal_remotebasedir_secondconfirm_vaultname": "重设回默认的库文件夹名", "modal_remotebasedir_secondconfirm_vaultname": "重设回默认的库文件夹名",
"modal_remotebasedir_secondconfirm_change": "确认修改", "modal_remotebasedir_secondconfirm_change": "确认修改",
"modal_remotebasedir_notice": "新的远端基文件夹设置已保存!", "modal_remotebasedir_notice": "新的远端基文件夹设置已保存!",
"modal_remoteprefix_title": "您正在修改远端路径前缀设置", "modal_remoteprefix_s3_title": "您正在修改远端路径前缀设置",
"modal_remoteprefix_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空那么本设置为保存为空文件将会被存储在桶Bucket的根目录。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。", "modal_remoteprefix_s3_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空那么本设置为保存为空文件将会被存储在桶Bucket的根目录。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。",
"modal_remoteprefix_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。", "modal_remoteprefix_s3_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。",
"modal_remoteprefix_tosave": "您设定的新前缀为:“{{{prefix}}}”", "modal_remoteprefix_s3_tosave": "您设定的新前缀为:“{{{prefix}}}”",
"modal_remoteprefix_secondconfirm_empty": "前缀为空,文件会保存在根目录", "modal_remoteprefix_s3_secondconfirm_empty": "前缀为空,文件会保存在根目录",
"modal_remoteprefix_secondconfirm_change": "确认修改", "modal_remoteprefix_s3_secondconfirm_change": "确认修改",
"modal_remoteprefix_notice": "新的远端路径前缀设置已保存!", "modal_remoteprefix_s3_notice": "新的远端路径前缀设置已保存!",
"modal_dropboxauth_manualsteps": "第 1 步:在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后您应该会获得一串很长的代码文本请复制粘贴到下方并点击“提交”", "modal_dropboxauth_manualsteps": "第 1 步:在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后您应该会获得一串很长的代码文本请复制粘贴到下方并点击“提交”",
"modal_dropboxauth_autosteps": "在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后您应该会被自动重定向回来 Obsidian。", "modal_dropboxauth_autosteps": "在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后您应该会被自动重定向回来 Obsidian。",
"modal_dropboxauth_copybutton": "点击此按钮从而复制鉴权 url", "modal_dropboxauth_copybutton": "点击此按钮从而复制鉴权 url",
@ -162,8 +162,8 @@
"settings_checkonnectivity_checking": "正在检查……", "settings_checkonnectivity_checking": "正在检查……",
"settings_remotebasedir": "修改远端基文件夹(实验性质)", "settings_remotebasedir": "修改远端基文件夹(实验性质)",
"settings_remotebasedir_desc": "默认设定,内容会被同步到远端的和资料库同名的文件夹下。您可以在此修改远端文件夹名,或删除输入框文本从而重设到默认值。您需要点击“确认”。", "settings_remotebasedir_desc": "默认设定,内容会被同步到远端的和资料库同名的文件夹下。您可以在此修改远端文件夹名,或删除输入框文本从而重设到默认值。您需要点击“确认”。",
"settings_remoteprefix": "修改远端前缀路径(实验性质)", "settings_remoteprefix_s3": "修改远端前缀路径(实验性质)",
"settings_remoteprefix_desc": "默认设定 s3 保存在存储桶Bucket的根目录。您可以在这里修改路径前缀或者保持为空保持默认设置。您需要点击“确认”。", "settings_remoteprefix_s3_desc": "默认设定 s3 保存在存储桶Bucket的根目录。您可以在这里修改路径前缀或者保持为空保持默认设置。您需要点击“确认”。",
"settings_s3": "S3 或兼容 S3 的服务的设置", "settings_s3": "S3 或兼容 S3 的服务的设置",
"settings_s3_disclaimer1": "声明:本插件不是 Amazon 的官方产品。", "settings_s3_disclaimer1": "声明:本插件不是 Amazon 的官方产品。",
"settings_s3_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了存储桶有不符合预期的访问,请立刻从 AWS或其它 S3 服务商)删除记录于此的 access key。", "settings_s3_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了存储桶有不符合预期的访问,请立刻从 AWS或其它 S3 服务商)删除记录于此的 access key。",

View File

@ -81,13 +81,13 @@
"modal_remotebasedir_secondconfirm_vaultname": "重設回預設的庫資料夾名", "modal_remotebasedir_secondconfirm_vaultname": "重設回預設的庫資料夾名",
"modal_remotebasedir_secondconfirm_change": "確認修改", "modal_remotebasedir_secondconfirm_change": "確認修改",
"modal_remotebasedir_notice": "新的遠端基資料夾設定已儲存!", "modal_remotebasedir_notice": "新的遠端基資料夾設定已儲存!",
"modal_remoteprefix_title": "您正在修改遠端路徑字首設定", "modal_remoteprefix_s3_title": "您正在修改遠端路徑字首設定",
"modal_remoteprefix_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空那麼本設定為儲存為空檔案將會被儲存在桶Bucket的根目錄。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。", "modal_remoteprefix_s3_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空那麼本設定為儲存為空檔案將會被儲存在桶Bucket的根目錄。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。",
"modal_remoteprefix_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。", "modal_remoteprefix_s3_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。",
"modal_remoteprefix_tosave": "您設定的新字首為:“{{{prefix}}}”", "modal_remoteprefix_s3_tosave": "您設定的新字首為:“{{{prefix}}}”",
"modal_remoteprefix_secondconfirm_empty": "字首為空,檔案會儲存在根目錄", "modal_remoteprefix_s3_secondconfirm_empty": "字首為空,檔案會儲存在根目錄",
"modal_remoteprefix_secondconfirm_change": "確認修改", "modal_remoteprefix_s3_secondconfirm_change": "確認修改",
"modal_remoteprefix_notice": "新的遠端路徑字首設定已儲存!", "modal_remoteprefix_s3_notice": "新的遠端路徑字首設定已儲存!",
"modal_dropboxauth_manualsteps": "第 1 步:在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後您應該會獲得一串很長的程式碼文字請複製貼上到下方並點選“提交”", "modal_dropboxauth_manualsteps": "第 1 步:在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後您應該會獲得一串很長的程式碼文字請複製貼上到下方並點選“提交”",
"modal_dropboxauth_autosteps": "在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後您應該會被自動重定向回來 Obsidian。", "modal_dropboxauth_autosteps": "在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後您應該會被自動重定向回來 Obsidian。",
"modal_dropboxauth_copybutton": "點選此按鈕從而複製鑑權 url", "modal_dropboxauth_copybutton": "點選此按鈕從而複製鑑權 url",
@ -161,8 +161,8 @@
"settings_checkonnectivity_checking": "正在檢查……", "settings_checkonnectivity_checking": "正在檢查……",
"settings_remotebasedir": "修改遠端基資料夾(實驗性質)", "settings_remotebasedir": "修改遠端基資料夾(實驗性質)",
"settings_remotebasedir_desc": "預設設定,內容會被同步到遠端的和資料庫同名的資料夾下。您可以在此修改遠端資料夾名,或刪除輸入框文字從而重設到預設值。您需要點選“確認”。", "settings_remotebasedir_desc": "預設設定,內容會被同步到遠端的和資料庫同名的資料夾下。您可以在此修改遠端資料夾名,或刪除輸入框文字從而重設到預設值。您需要點選“確認”。",
"settings_remoteprefix": "修改遠端字首路徑(實驗性質)", "settings_remoteprefix_s3": "修改遠端字首路徑(實驗性質)",
"settings_remoteprefix_desc": "預設設定 s3 儲存在儲存桶Bucket的根目錄。您可以在這裡修改路徑字首或者保持為空保持預設設定。您需要點選“確認”。", "settings_remoteprefix_s3_desc": "預設設定 s3 儲存在儲存桶Bucket的根目錄。您可以在這裡修改路徑字首或者保持為空保持預設設定。您需要點選“確認”。",
"settings_s3": "S3 或相容 S3 的服務的設定", "settings_s3": "S3 或相容 S3 的服務的設定",
"settings_s3_disclaimer1": "宣告:本外掛不是 Amazon 的官方產品。", "settings_s3_disclaimer1": "宣告:本外掛不是 Amazon 的官方產品。",
"settings_s3_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了儲存桶有不符合預期的訪問,請立刻從 AWS或其它 S3 服務商)刪除記錄於此的 access key。", "settings_s3_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了儲存桶有不符合預期的訪問,請立刻從 AWS或其它 S3 服務商)刪除記錄於此的 access key。",

View File

@ -30,6 +30,7 @@ import {
COMMAND_CALLBACK_PRO, COMMAND_CALLBACK_PRO,
COMMAND_CALLBACK_YANDEXDISK, COMMAND_CALLBACK_YANDEXDISK,
} from "../pro/src/baseTypesPro"; } from "../pro/src/baseTypesPro";
import { DEFAULT_AZUREBLOBSTORAGE_CONFIG } from "../pro/src/fsAzureBlobStorage";
import { import {
DEFAULT_BOX_CONFIG, DEFAULT_BOX_CONFIG,
FakeFsBox, FakeFsBox,
@ -115,6 +116,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
pcloud: DEFAULT_PCLOUD_CONFIG, pcloud: DEFAULT_PCLOUD_CONFIG,
yandexdisk: DEFAULT_YANDEXDISK_CONFIG, yandexdisk: DEFAULT_YANDEXDISK_CONFIG,
koofr: DEFAULT_KOOFR_CONFIG, koofr: DEFAULT_KOOFR_CONFIG,
azureblobstorage: DEFAULT_AZUREBLOBSTORAGE_CONFIG,
password: "", password: "",
serviceType: "s3", serviceType: "s3",
currLogLevel: "info", currLogLevel: "info",
@ -1376,6 +1378,10 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.koofr = DEFAULT_KOOFR_CONFIG; this.settings.koofr = DEFAULT_KOOFR_CONFIG;
} }
if (this.settings.azureblobstorage === undefined) {
this.settings.azureblobstorage = DEFAULT_AZUREBLOBSTORAGE_CONFIG;
}
await this.saveSettings(); await this.saveSettings();
} }

View File

@ -21,6 +21,7 @@ import type {
} from "./baseTypes"; } from "./baseTypes";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import { generateAzureBlobStorageSettingsPart } from "../pro/src/settingsAzureBlobStorage";
import { generateBoxSettingsPart } from "../pro/src/settingsBox"; import { generateBoxSettingsPart } from "../pro/src/settingsBox";
import { generateGoogleDriveSettingsPart } from "../pro/src/settingsGoogleDrive"; import { generateGoogleDriveSettingsPart } from "../pro/src/settingsGoogleDrive";
import { generateKoofrSettingsPart } from "../pro/src/settingsKoofr"; 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 * s3 is special and do not necessarily the same as others
* thus a new Modal here * thus a new Modal here
*/ */
class ChangeRemotePrefixModal extends Modal { class ChangeS3RemotePrefixModal extends Modal {
readonly plugin: RemotelySavePlugin; readonly plugin: RemotelySavePlugin;
readonly newRemotePrefix: string; readonly newRemotePrefix: string;
constructor(app: App, plugin: RemotelySavePlugin, newRemotePrefix: string) { constructor(app: App, plugin: RemotelySavePlugin, newRemotePrefix: string) {
@ -306,8 +307,8 @@ class ChangeRemotePrefixModal extends Modal {
return this.plugin.i18n.t(x, vars); return this.plugin.i18n.t(x, vars);
}; };
contentEl.createEl("h2", { text: t("modal_remoteprefix_title") }); contentEl.createEl("h2", { text: t("modal_remoteprefix_s3_title") });
t("modal_remoteprefix_shortdesc") t("modal_remoteprefix_s3_shortdesc")
.split("\n") .split("\n")
.forEach((val, idx) => { .forEach((val, idx) => {
contentEl.createEl("p", { contentEl.createEl("p", {
@ -316,7 +317,7 @@ class ChangeRemotePrefixModal extends Modal {
}); });
contentEl.createEl("p", { contentEl.createEl("p", {
text: t("modal_remoteprefix_tosave", { prefix: this.newRemotePrefix }), text: t("modal_remoteprefix_s3_tosave", { prefix: this.newRemotePrefix }),
}); });
if ( if (
@ -325,12 +326,12 @@ class ChangeRemotePrefixModal extends Modal {
) { ) {
new Setting(contentEl) new Setting(contentEl)
.addButton((button) => { .addButton((button) => {
button.setButtonText(t("modal_remoteprefix_secondconfirm_empty")); button.setButtonText(t("modal_remoteprefix_s3_secondconfirm_empty"));
button.onClick(async () => { button.onClick(async () => {
// in the settings, the value is reset to the special case "" // in the settings, the value is reset to the special case ""
this.plugin.settings.s3.remotePrefix = ""; this.plugin.settings.s3.remotePrefix = "";
await this.plugin.saveSettings(); await this.plugin.saveSettings();
new Notice(t("modal_remoteprefix_notice")); new Notice(t("modal_remoteprefix_s3_notice"));
this.close(); this.close();
}); });
button.setClass("remoteprefix-second-confirm"); button.setClass("remoteprefix-second-confirm");
@ -344,14 +345,14 @@ class ChangeRemotePrefixModal extends Modal {
} else { } else {
new Setting(contentEl) new Setting(contentEl)
.addButton((button) => { .addButton((button) => {
button.setButtonText(t("modal_remoteprefix_secondconfirm_change")); button.setButtonText(t("modal_remoteprefix_s3_secondconfirm_change"));
button.onClick(async () => { button.onClick(async () => {
this.plugin.settings.s3.remotePrefix = this.newRemotePrefix; this.plugin.settings.s3.remotePrefix = this.newRemotePrefix;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
new Notice(t("modal_remoteprefix_notice")); new Notice(t("modal_remoteprefix_s3_notice"));
this.close(); this.close();
}); });
button.setClass("remoteprefix-second-confirm"); button.setClass("remoteprefix-s3-second-confirm");
}) })
.addButton((button) => { .addButton((button) => {
button.setButtonText(t("goback")); button.setButtonText(t("goback"));
@ -801,7 +802,7 @@ const getEyesElements = () => {
}; };
}; };
const wrapTextWithPasswordHide = (text: TextComponent) => { export const wrapTextWithPasswordHide = (text: TextComponent) => {
const { eye, eyeOff } = getEyesElements(); const { eye, eyeOff } = getEyesElements();
const hider = text.inputEl.insertAdjacentElement("afterend", createSpan())!; const hider = text.inputEl.insertAdjacentElement("afterend", createSpan())!;
// the init type of hider is "hidden" === eyeOff === password // 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 || ""; let newS3RemotePrefix = this.plugin.settings.s3.remotePrefix || "";
new Setting(s3Div) new Setting(s3Div)
.setName(t("settings_remoteprefix")) .setName(t("settings_remoteprefix_s3"))
.setDesc(t("settings_remoteprefix_desc")) .setDesc(t("settings_remoteprefix_s3_desc"))
.addText((text) => .addText((text) =>
text text
.setPlaceholder("") .setPlaceholder("")
@ -1067,7 +1068,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.addButton((button) => { .addButton((button) => {
button.setButtonText(t("confirm")); button.setButtonText(t("confirm"));
button.onClick(() => { button.onClick(() => {
new ChangeRemotePrefixModal( new ChangeS3RemotePrefixModal(
this.app, this.app,
this.plugin, this.plugin,
simpleTransRemotePrefix(newS3RemotePrefix.trim()) simpleTransRemotePrefix(newS3RemotePrefix.trim())
@ -1873,6 +1874,22 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.saveSettings() 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) // below for general chooser (part 2/2)
////////////////////////////////////////////////// //////////////////////////////////////////////////
@ -1899,6 +1916,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
t("settings_chooseservice_yandexdisk") t("settings_chooseservice_yandexdisk")
); );
dropdown.addOption("koofr", t("settings_chooseservice_koofr")); dropdown.addOption("koofr", t("settings_chooseservice_koofr"));
dropdown.addOption(
"azureblobstorage",
t("settings_chooseservice_azureblobstorage")
);
dropdown dropdown
.setValue(this.plugin.settings.serviceType) .setValue(this.plugin.settings.serviceType)
@ -1944,6 +1965,11 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
"koofr-hide", "koofr-hide",
this.plugin.settings.serviceType !== "koofr" this.plugin.settings.serviceType !== "koofr"
); );
azureBlobStorageDiv.toggleClass(
"azureblobstorage-hide",
this.plugin.settings.serviceType !== "azureblobstorage"
);
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}); });
}); });
@ -2542,6 +2568,16 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
button.onClick(async () => { button.onClick(async () => {
new ExportSettingsQrCodeModal(this.app, this.plugin, "koofr").open(); 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 = ""; let importSettingVal = "";
@ -2616,7 +2652,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
yandexDiskAllowedToUsedDiv, yandexDiskAllowedToUsedDiv,
yandexDiskNotShowUpHintSetting, yandexDiskNotShowUpHintSetting,
koofrAllowedToUsedDiv, koofrAllowedToUsedDiv,
koofrNotShowUpHintSetting koofrNotShowUpHintSetting,
azureBlobStorageAllowedToUsedDiv,
azureBlobStorageNotShowUpHintSetting
); );
////////////////////////////////////////////////// //////////////////////////////////////////////////

View File

@ -171,6 +171,17 @@
display: none; display: none;
} }
.azureblobstorage-disclaimer {
font-weight: bold;
}
.azureblobstorage-hide {
display: none;
}
.azureblobstorage-allow-to-use-hide {
display: none;
}
.qrcode-img { .qrcode-img {
width: 350px; width: 350px;
height: 350px; height: 350px;

View File

@ -34,6 +34,9 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
koofr: { koofr: {
refreshToken: "xxx", refreshToken: "xxx",
} as any, } as any,
azureblobstorage: {
containerSasUrl: "http://127.0.0.1",
} as any,
password: "password", password: "password",
serviceType: "s3", serviceType: "s3",
currLogLevel: "info", currLogLevel: "info",

View File

@ -16,5 +16,5 @@
"isolatedModules": true, "isolatedModules": true,
"lib": ["dom", "es5", "scripthost", "es2015", "webworker"] "lib": ["dom", "es5", "scripthost", "es2015", "webworker"]
}, },
"include": ["**/*.ts"] "include": ["src/global.d.ts", "**/*.ts"]
} }

View File

@ -31,30 +31,37 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`, "global.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`, "global.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
"process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`, "global.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`,
"process.env.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`, "global.DEFAULT_REMOTELYSAVE_WEBSITE": `"${DEFAULT_REMOTELYSAVE_WEBSITE}"`,
"process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`, "global.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`,
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`, "global.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`,
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`, "global.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`,
"process.env.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`, "global.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`,
"process.env.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`, "global.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`,
"process.env.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`, "global.DEFAULT_PCLOUD_CLIENT_ID": `"${DEFAULT_PCLOUD_CLIENT_ID}"`,
"process.env.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`, "global.DEFAULT_PCLOUD_CLIENT_SECRET": `"${DEFAULT_PCLOUD_CLIENT_SECRET}"`,
"process.env.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`, "global.DEFAULT_YANDEXDISK_CLIENT_ID": `"${DEFAULT_YANDEXDISK_CLIENT_ID}"`,
"process.env.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`, "global.DEFAULT_YANDEXDISK_CLIENT_SECRET": `"${DEFAULT_YANDEXDISK_CLIENT_SECRET}"`,
"process.env.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`, "global.DEFAULT_KOOFR_CLIENT_ID": `"${DEFAULT_KOOFR_CLIENT_ID}"`,
"process.env.DEFAULT_KOOFR_CLIENT_SECRET": `"${DEFAULT_KOOFR_CLIENT_SECRET}"`, "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: // Work around for Buffer is undefined:
// https://github.com/webpack/changelog-v5/issues/10 // https://github.com/webpack/changelog-v5/issues/10
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"], Buffer: ["buffer", "Buffer"],
}), }),
new webpack.ProvidePlugin({ // new webpack.ProvidePlugin({
process: "process/browser", // process: "process/browser",
}), // }),
], ],
module: { module: {
rules: [ rules: [
@ -103,7 +110,7 @@ module.exports = {
// os: require.resolve("os-browserify/browser"), // os: require.resolve("os-browserify/browser"),
path: require.resolve("path-browserify"), path: require.resolve("path-browserify"),
// punycode: require.resolve("punycode"), // punycode: require.resolve("punycode"),
process: require.resolve("process/browser"), process: false, // require.resolve("process/browser"),
// querystring: require.resolve("querystring-es3"), // querystring: require.resolve("querystring-es3"),
stream: require.resolve("stream-browserify"), stream: require.resolve("stream-browserify"),
// string_decoder: require.resolve("string_decoder"), // string_decoder: require.resolve("string_decoder"),