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);
+});