diff --git a/src/components/SetupRepository.jsx b/src/components/SetupRepository.jsx index d5438a4a..7b700052 100644 --- a/src/components/SetupRepository.jsx +++ b/src/components/SetupRepository.jsx @@ -18,6 +18,7 @@ import { SetupRepositoryFilesystem } from "./SetupRepositoryFilesystem"; import { SetupRepositoryGCS } from "./SetupRepositoryGCS"; import { SetupRepositoryServer } from "./SetupRepositoryServer"; import { SetupRepositoryRclone } from "./SetupRepositoryRclone"; +import { SetupRepositoryR2 } from "./SetupRepositoryR2"; import { SetupRepositoryS3 } from "./SetupRepositoryS3"; import { SetupRepositorySFTP } from "./SetupRepositorySFTP"; import { SetupRepositoryToken } from "./SetupRepositoryToken"; @@ -40,6 +41,11 @@ const supportedProviders = [ description: "Amazon S3 or Compatible Storage", component: SetupRepositoryS3, }, + { + provider: "r2", + description: "Cloudflare R2", + component: SetupRepositoryR2, + }, { provider: "b2", description: "Backblaze B2", component: SetupRepositoryB2 }, { provider: "azureBlob", diff --git a/src/components/SetupRepositoryR2.jsx b/src/components/SetupRepositoryR2.jsx new file mode 100644 index 00000000..2b1e0bee --- /dev/null +++ b/src/components/SetupRepositoryR2.jsx @@ -0,0 +1,87 @@ +import React, { Component } from "react"; +import Col from "react-bootstrap/Col"; +import Form from "react-bootstrap/Form"; +import Row from "react-bootstrap/Row"; +import { handleChange, validateRequiredFields } from "../forms"; +import { OptionalField } from "../forms/OptionalField"; +import { RequiredBoolean } from "../forms/RequiredBoolean"; +import { RequiredField } from "../forms/RequiredField"; +import PropTypes from "prop-types"; + +export class SetupRepositoryR2 extends Component { + constructor(props) { + super(); + + this.state = { + jurisdiction: "default", + doNotUseTLS: false, + doNotVerifyTLS: false, + ...props.initial, + }; + this.handleChange = handleChange.bind(this); + } + + validate() { + return validateRequiredFields(this, ["accountID", "bucket", "accessKeyID", "secretAccessKey"]); + } + + render() { + return ( + <> + + {RequiredField(this, "Account ID", "accountID", { + autoFocus: true, + placeholder: "enter Cloudflare account ID", + })} + {RequiredField(this, "Bucket", "bucket", { + placeholder: "enter bucket name", + })} + + Jurisdiction + + + + + + + + + {OptionalField(this, "Endpoint Override", "endpoint", { + placeholder: "leave empty to derive from account ID", + })} + {OptionalField(this, "Object Name Prefix", "prefix", { + placeholder: "enter object name prefix or leave empty", + })} + + + {RequiredBoolean(this, "Use HTTP connection (insecure)", "doNotUseTLS")} + {RequiredBoolean(this, "Do not verify TLS certificate", "doNotVerifyTLS")} + + + {RequiredField(this, "Access Key ID", "accessKeyID", { + placeholder: "enter access key ID", + })} + {RequiredField(this, "Secret Access Key", "secretAccessKey", { + placeholder: "enter secret access key", + type: "password", + })} + {OptionalField(this, "Session Token", "sessionToken", { + placeholder: "enter session token or leave empty", + type: "password", + })} + + + ); + } +} + +SetupRepositoryR2.propTypes = { + initial: PropTypes.object, +}; diff --git a/tests/components/SetupRepository.test.jsx b/tests/components/SetupRepository.test.jsx index e1c14f27..fad26629 100644 --- a/tests/components/SetupRepository.test.jsx +++ b/tests/components/SetupRepository.test.jsx @@ -73,6 +73,35 @@ it("can connect to existing repository when already initialized", async () => { await waitFor(() => serverMock.history.post.length == 1); }); +it("can verify Cloudflare R2 storage", async () => { + serverMock + .onPost("/api/v1/repo/exists", { + storage: { + type: "r2", + config: { + accountID: "some-account-id", + bucket: "some-bucket", + jurisdiction: "default", + doNotUseTLS: false, + doNotVerifyTLS: false, + accessKeyID: "some-access-key-id", + secretAccessKey: "some-secret-access-key", + }, + }, + }) + .reply(200, {}); + + const { getByTestId, container } = await act(() => render()); + fireEvent.click(getByTestId("provider-r2")); + fireEvent.change(await findByTestId(container, "control-accountID"), { target: { value: "some-account-id" } }); + fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); + fireEvent.change(getByTestId("control-accessKeyID"), { target: { value: "some-access-key-id" } }); + fireEvent.change(getByTestId("control-secretAccessKey"), { target: { value: "some-secret-access-key" } }); + + await act(() => fireEvent.click(getByTestId("submit-button"))); + await waitFor(() => serverMock.history.post.length == 1); +}); + it("can connect to existing repository using token", async () => { serverMock .onPost("/api/v1/repo/connect", { diff --git a/tests/components/SetupRepositoryR2.test.jsx b/tests/components/SetupRepositoryR2.test.jsx new file mode 100644 index 00000000..ce4f87d9 --- /dev/null +++ b/tests/components/SetupRepositoryR2.test.jsx @@ -0,0 +1,42 @@ +import { fireEvent, render, act } from "@testing-library/react"; +import React from "react"; +import { SetupRepositoryR2 } from "../../src/components/SetupRepositoryR2"; + +it("can set fields", async () => { + let ref = React.createRef(); + const { getByTestId } = render(); + + act(() => expect(ref.current.validate()).toBe(false)); + // required + fireEvent.change(getByTestId("control-accountID"), { target: { value: "some-accountID" } }); + fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); + fireEvent.change(getByTestId("control-accessKeyID"), { target: { value: "some-accessKeyID" } }); + fireEvent.change(getByTestId("control-secretAccessKey"), { target: { value: "some-secretAccessKey" } }); + act(() => expect(ref.current.validate()).toBe(true)); + // optional + fireEvent.change(getByTestId("control-jurisdiction"), { target: { value: "eu" } }); + fireEvent.change(getByTestId("control-endpoint"), { target: { value: "some-endpoint" } }); + fireEvent.click(getByTestId("control-doNotUseTLS")); + fireEvent.click(getByTestId("control-doNotVerifyTLS")); + fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); + fireEvent.change(getByTestId("control-sessionToken"), { target: { value: "some-sessionToken" } }); + act(() => expect(ref.current.validate()).toBe(true)); + + expect(ref.current.state).toStrictEqual({ + accountID: "some-accountID", + accessKeyID: "some-accessKeyID", + bucket: "some-bucket", + endpoint: "some-endpoint", + prefix: "some-prefix", + jurisdiction: "eu", + doNotUseTLS: true, + doNotVerifyTLS: true, + secretAccessKey: "some-secretAccessKey", + sessionToken: "some-sessionToken", + }); + + fireEvent.click(getByTestId("control-doNotUseTLS")); + fireEvent.click(getByTestId("control-doNotVerifyTLS")); + expect(ref.current.state.doNotUseTLS).toBe(false); + expect(ref.current.state.doNotVerifyTLS).toBe(false); +});