Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion documentation/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,27 @@ Otherwise, if you want to upload files to a specific folder, do the following st
However, if desired, you can also create another subfolder there.
3. Navigate to the exact folder where you want the files to be uploaded.
4. Look at the URL bar of your browser and copy the value of the `folder=<FOLDER_ID>` parameter
into the `PCLOUD_FOLDER_ID` environment variable.
into the `PCLOUD_FOLDER_ID` environment variable.

### AWS S3

1. Create an AWS account if you don't have one already
2. Create an S3 bucket where you want to store your Notion backups
3. Create an IAM user with programmatic access:
- Go to the [IAM Console](https://console.aws.amazon.com/iam/)
- Click on "Users" and then "Add user"
- Give the user a name (e.g., "notion-backup")
- Select "Access key - Programmatic access"
- Click "Next: Permissions"
- Click "Attach existing policies directly"
- Search for and select `AmazonS3FullAccess` (or create a more restricted policy if desired)
- Complete the user creation process
4. After creating the user, you'll see the access key and secret key. Add these to your `.env` file:
```
AWS_ACCESS_KEY=your_access_key
AWS_SECRET_KEY=your_secret_key
AWS_REGION=your_region (e.g., us-east-1)
AWS_BUCKET_NAME=your_bucket_name
```

Note: Make sure your S3 bucket exists before running the application. The application will create a new file in the bucket with the same name as the exported Notion file.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@
<version>1.8.1</version>
</dependency>

<!-- AWS SDK -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.261</version>
</dependency>

<!-- LOMBOK -->
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down
46 changes: 45 additions & 1 deletion src/main/java/com/greydev/notionbackup/NotionBackup.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@

import io.github.cdimascio.dotenv.Dotenv;
import lombok.extern.slf4j.Slf4j;
import com.amazonaws.services.s3.AmazonS3;
import com.greydev.notionbackup.cloudstorage.aws.S3Client;
import com.greydev.notionbackup.cloudstorage.aws.S3ServiceFactory;


@Slf4j
Expand All @@ -51,6 +54,11 @@ public class NotionBackup {
private static final String KEY_PCLOUD_API_HOST = "PCLOUD_API_HOST";
private static final String KEY_PCLOUD_FOLDER_ID = "PCLOUD_FOLDER_ID";

private static final String KEY_AWS_ACCESS_KEY = "AWS_ACCESS_KEY";
private static final String KEY_AWS_SECRET_KEY = "AWS_SECRET_KEY";
private static final String KEY_AWS_REGION = "AWS_REGION";
private static final String KEY_AWS_BUCKET_NAME = "AWS_BUCKET_NAME";

private static final Dotenv dotenv;

static {
Expand Down Expand Up @@ -110,7 +118,17 @@ public static void main(String[] args) {
return null;
});

CompletableFuture.allOf(futureGoogleDrive, futureDropbox, futureNextcloud, futurePCloud).join();
CompletableFuture<Void> futureS3 = CompletableFuture
.runAsync(() -> NotionBackup.startS3Backup(exportedFile))
.handle((result, ex) -> {
if (ex != null) {
hasErrorOccurred.set(true);
log.error("Exception while S3 upload", ex);
}
return null;
});

CompletableFuture.allOf(futureGoogleDrive, futureDropbox, futureNextcloud, futurePCloud, futureS3).join();

if (hasErrorOccurred.get()) {
log.error("Not all backups were completed successfully. See the logs above to get more information about the errors.");
Expand Down Expand Up @@ -245,6 +263,32 @@ public static void startPCloudBackup(File fileToUpload) {
}
}

public static void startS3Backup(File fileToUpload) {
String accessKey = dotenv.get(KEY_AWS_ACCESS_KEY);
String secretKey = dotenv.get(KEY_AWS_SECRET_KEY);
String region = dotenv.get(KEY_AWS_REGION);
String bucketName = dotenv.get(KEY_AWS_BUCKET_NAME);

if (StringUtils.isAnyBlank(accessKey, secretKey, region, bucketName)) {
log.info("Skipping S3 upload. {}, {}, {} or {} is blank.",
KEY_AWS_ACCESS_KEY, KEY_AWS_SECRET_KEY, KEY_AWS_REGION, KEY_AWS_BUCKET_NAME);
return;
}

Optional<AmazonS3> s3Client = S3ServiceFactory.create(accessKey, secretKey, region);
if (s3Client.isEmpty()) {
log.warn("Could not create S3 client. Skipping S3 upload.");
return;
}

S3Client client = new S3Client(s3Client.get(), bucketName);
boolean isSuccess = client.upload(fileToUpload);

if (!isSuccess) {
throw new IllegalStateException("Backup was not successful");
}
}

private static Optional<String> extractGoogleServiceAccountSecret() {
String serviceAccountSecret = dotenv.get(KEY_GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_JSON);
String serviceAccountSecretFilePath = dotenv.get(KEY_GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_FILE_PATH);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.greydev.notionbackup.cloudstorage.aws;

import java.io.File;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.greydev.notionbackup.cloudstorage.CloudStorageClient;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class S3Client implements CloudStorageClient {

private final AmazonS3 s3Client;
private final String bucketName;

public S3Client(AmazonS3 s3Client, String bucketName) {
this.s3Client = s3Client;
this.bucketName = bucketName;
}

@Override
public boolean upload(File fileToUpload) {
log.info("S3: uploading file '{}' ...", fileToUpload.getName());
if (!(fileToUpload.exists() && fileToUpload.isFile())) {
log.error("S3: could not find {} in project root directory", fileToUpload.getName());
return false;
}

try {
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileToUpload.getName(), fileToUpload);
s3Client.putObject(putObjectRequest);
log.info("S3: successfully uploaded '{}'", fileToUpload.getName());
return true;
} catch (Exception e) {
log.error("S3: Error uploading file", e);
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.greydev.notionbackup.cloudstorage.aws;

import java.util.Optional;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class S3ServiceFactory {

public static Optional<AmazonS3> create(String accessKey, String secretKey, String region) {
try {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
return Optional.of(s3Client);
} catch (Exception e) {
log.error("Error creating S3 client", e);
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.greydev.notionbackup.cloudstorage.aws;

import java.io.File;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.PutObjectRequest;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

@ExtendWith(MockitoExtension.class)
class S3ClientTest {

@Mock
private AmazonS3 s3Client;

@Test
void testUpload() {
// given
File fileToUpload = new File("src/test/resources/testFileToUpload.txt");
String bucketName = "test-bucket";
S3Client client = new S3Client(s3Client, bucketName);

// when
boolean result = client.upload(fileToUpload);

// then
assertTrue(result);
verify(s3Client).putObject(any(PutObjectRequest.class));
}

@Test
void testUpload_Exception() {
// given
File fileToUpload = new File("src/test/resources/testFileToUpload.txt");
String bucketName = "test-bucket";
S3Client client = new S3Client(s3Client, bucketName);

doThrow(new RuntimeException("S3 error")).when(s3Client).putObject(any(PutObjectRequest.class));

// when
boolean result = client.upload(fileToUpload);

// then
assertFalse(result);
verify(s3Client).putObject(any(PutObjectRequest.class));
}

@Test
void testUpload_invalidFile() {
// given
File fileToUpload = new File("thisFileDoesNotExist.txt");
String bucketName = "test-bucket";
S3Client client = new S3Client(s3Client, bucketName);

// when
boolean result = client.upload(fileToUpload);

// then
assertFalse(result);
verifyNoInteractions(s3Client);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.greydev.notionbackup.cloudstorage.aws;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

class S3ServiceFactoryTest {

@Test
void testCreate_ValidCredentials() {
// given
String accessKey = "test-access-key";
String secretKey = "test-secret-key";
String region = "us-east-1";

// when
var result = S3ServiceFactory.create(accessKey, secretKey, region);

// then
assertTrue(result.isPresent());
}

@Test
void testCreate_InvalidCredentials() {
// given
String accessKey = null;
String secretKey = null;
String region = "invalid-region";

// when
var result = S3ServiceFactory.create(accessKey, secretKey, region);

// then
assertFalse(result.isPresent());
}
}