diff --git a/tutorials/client/attachments-and-files/aws-s3-storage-adapter.mdx b/tutorials/client/attachments-and-files/aws-s3-storage-adapter.mdx index f9a6be16..a3826b8c 100644 --- a/tutorials/client/attachments-and-files/aws-s3-storage-adapter.mdx +++ b/tutorials/client/attachments-and-files/aws-s3-storage-adapter.mdx @@ -4,6 +4,18 @@ description: "In this tutorial, we will show you how to replace Supabase Storage sidebarTitle: "AWS S3 Storage" --- +# Introduction + +The AWS credentials should never be exposed directly on the client - it could expose access to the entire S3 bucket to the user. +For this tutorial we have therefore decided to use the following workflow: +1. Client makes an API call to the app backend, using the client credentials (a [Supabase Edge Function](https://supabase.com/docs/guides/functions)). +2. The backend API has the S3 credentials. It signs a S3 upload/download URL, and returns that to the client. +3. The client uploads/downloads using the pre-signed S3 URL. + +The following updates to the [React Native To-Do List demo app](https://github.com/powersync-ja/powersync-js/tree/main/demos/react-native-supabase-todolist) are therefore required: +1. Create Supabase Edge Functions, and +2. Update the demo app to use the AWS S3 storage adapter + The following pre-requisites are required to complete this tutorial: - Clone the [To-Do List demo app](https://github.com/powersync-ja/powersync-js/tree/main/demos/react-native-supabase-todolist) repo @@ -41,94 +53,233 @@ sidebarTitle: "AWS S3 Storage" - - - - Add the following dependencies to the `package.json` file in the `demos/react-native-supabase-todolist` directory: - ```json - "react-navigation-stack": "^2.10.4", - "react-native-crypto": "^2.2.0", - "react-native-randombytes": "^3.6.1", - "aws-sdk": "^2.1352.0" - ``` - - - Run `pnpm install` to install the new dependencies. - - - - + + We need to create 3 Supabase Edge Functions to handle the S3 operations: + 1. Upload, + 2. Download, and + 3. Delete - - Add the following environment variables to the `.env` file and update the values with your AWS S3 configuration created in [Step 1](#step-1-aws-s3-setup): + Before we create the Edge Functions, we need to set up the environment variables for the AWS S3 credentials. Create an `.env` file in the root of your Supabase project, add and update + the values with your AWS S3 configuration created in [Step 1](#step-1-aws-s3-setup): ```bash .env - ... - EXPO_PUBLIC_AWS_S3_REGION=region - EXPO_PUBLIC_AWS_S3_BUCKET_NAME=bucket_name - EXPO_PUBLIC_AWS_S3_ACCESS_KEY_ID=*** - EXPO_PUBLIC_AWS_S3_ACCESS_SECRET_ACCESS_KEY=*** + AWS_ACCESS_KEY_ID=*** + AWS_SECRET_ACCESS_KEY=*** + AWS_S3_REGION=#region + AWS_S3_BUCKET_NAME=#bucket_name ``` - - - - - - Update `process-env.d.ts` in the `demos/react-native-supabase-todolist` directory and add the following highlighted lines: - ```typescript process-env.d.ts {12-15} - export {}; - - declare global { - namespace NodeJS { - interface ProcessEnv { - [key: string]: string | undefined; - EXPO_PUBLIC_SUPABASE_URL: string; - EXPO_PUBLIC_SUPABASE_ANON_KEY: string; - EXPO_PUBLIC_SUPABASE_BUCKET: string; - EXPO_PUBLIC_POWERSYNC_URL: string; - EXPO_PUBLIC_EAS_PROJECT_ID: string; - EXPO_PUBLIC_AWS_S3_REGION: string; - EXPO_PUBLIC_AWS_S3_BUCKET_NAME: string; - EXPO_PUBLIC_AWS_S3_ACCESS_KEY_ID: string; - EXPO_PUBLIC_AWS_S3_ACCESS_SECRET_ACCESS_KEY: string; + + For more information on getting started with a Supabase Edge Function, see the Supabase [Getting Started Guide](https://supabase.com/docs/guides/functions/quickstart). + + + **Security Note** + + The filename specified in each edge function request can pose security risks, such as enabling a user to overwrite another user’s files by using the same filename. + To mitigate this, a common approach is to generate a random prefix or directory for each file. + While it’s likely fine to omit this safeguard in the demo — since users can already read and delete any file — this should be addressed in a **production environment**. + + + Create the `s3-upload` Edge Function by running the following in your Supabase project: + ```bash + supabase functions new s3-upload + ``` + ```typescript index.ts + import { PutObjectCommand, S3Client } from "npm:@aws-sdk/client-s3"; + import { getSignedUrl } from "npm:@aws-sdk/s3-request-presigner"; + import "jsr:@supabase/functions-js/edge-runtime.d.ts"; + + const AWS_ACCESS_KEY_ID = Deno.env.get('AWS_ACCESS_KEY_ID')!; + const AWS_SECRET_ACCESS_KEY = Deno.env.get('AWS_SECRET_ACCESS_KEY')!; + const AWS_REGION = Deno.env.get('AWS_S3_REGION')!; + const AWS_BUCKET_NAME = Deno.env.get('AWS_S3_BUCKET_NAME')!; + const accessControlAllowOrigin = "*"; + + Deno.serve(async (req) => { + if (req.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Only POST requests are allowed' }), { + status: 405, + }); + } + const { fileName, mediaType, expiresIn } = await req.json(); + + if (!fileName || !mediaType) { + return new Response( + JSON.stringify({ error: 'Missing required fields: fileName, mediaType or data' }), + { status: 400 } + ); + } + try { + const s3Client = new S3Client({ + region: AWS_REGION, + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY } - } + }); + const expiry = expiresIn || 900; + const command = new PutObjectCommand({ + Bucket: AWS_BUCKET_NAME, + Key: fileName, + ContentType: mediaType + }); + const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: expiry }) + return new Response( + JSON.stringify({ + message: `UploadURL for ${fileName} created successfully.`, + uploadUrl: uploadUrl + }), + { status: 200, headers: { "Content-Type": "application/json", 'Access-Control-Allow-Origin': accessControlAllowOrigin } } + ); + } catch (err) { + return new Response(JSON.stringify({ error: `Error uploading file ${fileName}: ${err}`}), { + headers: { "Content-Type": "application/json" }, + status: 500, + }); } - ``` - - - Update `AppConfig.ts` in the `demos/react-native-supabase-todolist/library/supabase` directory and add the following highlighted lines: - ```typescript AppConfig.ts {6-9} - export const AppConfig = { - supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL, - supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY, - supabaseBucket: process.env.EXPO_PUBLIC_SUPABASE_BUCKET || '', - powersyncUrl: process.env.EXPO_PUBLIC_POWERSYNC_URL, - region: process.env.EXPO_PUBLIC_AWS_S3_REGION, - accessKeyId: process.env.EXPO_PUBLIC_AWS_S3_ACCESS_KEY_ID || '', - secretAccessKey: process.env.EXPO_PUBLIC_AWS_S3_ACCESS_SECRET_ACCESS_KEY || '', - s3bucketName: process.env.EXPO_PUBLIC_AWS_S3_BUCKET_NAME || '' - }; - ``` - - + }); + ``` + + + + Create the `s3-download` Edge Function by running the following in your Supabase project: + ```bash + supabase functions new s3-download + ``` + ```typescript index.ts + import { GetObjectCommand, S3Client } from "npm:@aws-sdk/client-s3"; + import { getSignedUrl } from "npm:@aws-sdk/s3-request-presigner"; + import "jsr:@supabase/functions-js/edge-runtime.d.ts"; + + const AWS_ACCESS_KEY_ID = Deno.env.get('AWS_ACCESS_KEY_ID')!; + const AWS_SECRET_ACCESS_KEY = Deno.env.get('AWS_SECRET_ACCESS_KEY')!; + const AWS_REGION = Deno.env.get('AWS_S3_REGION')!; + const AWS_BUCKET_NAME = Deno.env.get('AWS_S3_BUCKET_NAME')!; + const accessControlAllowOrigin = "*"; + + Deno.serve(async (req) => { + if (req.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Only POST requests are allowed' }), { + status: 405, + }); + } + const { fileName, expiresIn } = await req.json(); + if (!fileName) { + return new Response( + JSON.stringify({ error: 'Missing required field: fileName' }), + { status: 400 } + ); + } + try { + const s3Client = new S3Client({ + region: AWS_REGION, + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY + } + }); + const expiry = expiresIn || 900; + const command = new GetObjectCommand({ + Bucket: AWS_BUCKET_NAME, + Key: fileName + }); + const downloadUrl = await getSignedUrl(s3Client, command, { expiresIn: expiry }); + return new Response( + JSON.stringify({ + message: `DownloadURL for ${fileName} created successfully.`, + downloadUrl: downloadUrl + }), + { status: 200, headers: { "Content-Type": "application/json", 'Access-Control-Allow-Origin': accessControlAllowOrigin }} + ); + } catch (err) { + return new Response(JSON.stringify({ error: `Error downloading file ${fileName}: ${err}`}), { + headers: { "Content-Type": "application/json" }, + status: 500, + }); + } + }); + ``` + + + + Create the `s3-delete` Edge Function by running the following in your Supabase project: + ```bash + supabase functions new s3-delete + ``` + ```typescript index.ts + import { DeleteObjectCommand, S3Client } from "npm:@aws-sdk/client-s3"; + import "jsr:@supabase/functions-js/edge-runtime.d.ts"; + + const AWS_ACCESS_KEY_ID = Deno.env.get('AWS_ACCESS_KEY_ID')!; + const AWS_SECRET_ACCESS_KEY = Deno.env.get('AWS_SECRET_ACCESS_KEY')!; + const AWS_REGION = Deno.env.get('AWS_S3_REGION')!; + const AWS_BUCKET_NAME = Deno.env.get('AWS_S3_BUCKET_NAME')!; + + Deno.serve(async (req) => { + if (req.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Only POST requests are allowed' }), { + status: 405, + }); + } + const { fileName } = await req.json(); + if (!fileName) { + return new Response( + JSON.stringify({ error: 'Missing required field: fileName' }), + { status: 400 } + ); + } + try { + const s3Client = new S3Client({ + region: AWS_REGION, + credentials: { + accessKeyId: AWS_ACCESS_KEY_ID, + secretAccessKey: AWS_SECRET_ACCESS_KEY + } + }); + const command = new DeleteObjectCommand({ + Bucket: AWS_BUCKET_NAME, + Key: fileName + }); + await s3Client.send(command); + return new Response(JSON.stringify({ message: `${fileName} deleted successfully from ${AWS_BUCKET_NAME}.` }), { + headers: { "Content-Type": "application/json" }, + status: 200, + }); + } catch (err) { + return new Response(JSON.stringify({ error: `Error deleting ${fileName} from ${AWS_BUCKET_NAME}: ${err}`}), { + headers: { "Content-Type": "application/json" }, + status: 500, + }); + } + }); + ``` + + - + Create a `AWSStorageAdapter.ts` file in the `demos/react-native-supabase-todolist/library/storage` directory and add the following contents: ```typescript AWSStorageAdapter.ts import * as FileSystem from 'expo-file-system'; - import S3 from 'aws-sdk/clients/s3'; import { decode as decodeBase64 } from 'base64-arraybuffer'; import { StorageAdapter } from '@powersync/attachments'; - import { AppConfig } from '../supabase/AppConfig'; - - export interface S3StorageAdapterOptions { - client: S3; + import { SupabaseClient } from '@supabase/supabase-js'; + + interface S3Upload { + message: string; + uploadUrl: string; + } + + interface S3Download { + message: string; + downloadUrl: string; + } + + interface S3Delete { + message: string; } export class AWSStorageAdapter implements StorageAdapter { - constructor(private options: S3StorageAdapterOptions) {} + constructor( public client: SupabaseClient ) {} async uploadFile( filename: string, @@ -137,21 +288,32 @@ sidebarTitle: "AWS S3 Storage" mediaType?: string; } ): Promise { - if (!AppConfig.s3bucketName) { - throw new Error('AWS S3 bucket not configured in AppConfig.ts'); + + const response = await this.client.functions.invoke('s3-upload', { + body: { + fileName: filename, + mediaType: options?.mediaType + } + }); + + if (response.error || !response.data) { + throw new Error(`Failed to reach upload edge function, code=${response.error}`); } + const { uploadUrl } = response.data; try { - const body = Uint8Array.from(new Uint8Array(data)); - const params = { - Bucket: AppConfig.s3bucketName, - Key: filename, - Body: body, - ContentType: options?.mediaType - }; - - await this.options.client.upload(params).promise(); - console.log(`File uploaded successfully to ${AppConfig.s3bucketName}/${filename}`); + const body = new Uint8Array(data); + + const response = await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Length": body.length.toString(), + "Content-Type": options?.mediaType, + }, + body: body, + }); + + console.log(`File: ${filename} uploaded successfully.`); } catch (error) { console.error('Error uploading file:', error); throw error; @@ -159,25 +321,24 @@ sidebarTitle: "AWS S3 Storage" } async downloadFile(filePath: string): Promise { - const s3 = new S3({ - region: AppConfig.region, - accessKeyId: AppConfig.accessKeyId, - secretAccessKey: AppConfig.secretAccessKey + const response = await this.client.functions.invoke('s3-download', { + body: { + fileName: filePath + } }); - const params = { - Bucket: AppConfig.s3bucketName, - Key: filePath - }; + if (response.error || !response.data) { + throw new Error(`Failed to reach download edge function, code=${response.error}`); + } + + const { downloadUrl } = response.data; try { - const obj = await s3.getObject(params).promise(); - if (obj.Body) { - const data = await new Response(obj.Body as ReadableStream).arrayBuffer(); - return new Blob([data]); - } else { - throw new Error('Object body is undefined. Could not download file.'); - } + const downloadResponse = await fetch(downloadUrl, { + method: "GET", + }); + + return await downloadResponse.blob(); } catch (error) { console.error('Error downloading file:', error); throw error; @@ -194,19 +355,21 @@ sidebarTitle: "AWS S3 Storage" return; } - if (!AppConfig.s3bucketName) { - throw new Error('Supabase bucket not configured in AppConfig.ts'); - } - try { - const params = { - Bucket: AppConfig.s3bucketName, - Key: filename - }; - await this.options.client.deleteObject(params).promise(); - console.log(`${filename} deleted successfully from ${AppConfig.s3bucketName}.`); + const response = await this.client.functions.invoke('s3-delete', { + body: { + fileName: options?.filename, + } + }); + + if (response.error || !response.data) { + throw new Error(`Failed to reach delete edge function, code=${response.error}`); + } + + const { message } = response.data; + console.log(message); } catch (error) { - console.error(`Error deleting ${filename} from ${AppConfig.s3bucketName}:`, error); + console.error(`Error deleting ${filename}:`, error); } } @@ -263,8 +426,8 @@ sidebarTitle: "AWS S3 Storage" } /** - * Converts a base64 string to an ArrayBuffer - */ + * Converts a base64 string to an ArrayBuffer + */ async base64ToArrayBuffer(base64: string): Promise { return decodeBase64(base64); } @@ -279,20 +442,56 @@ sidebarTitle: "AWS S3 Storage" ```typescript async uploadFile(filename: string, data: ArrayBuffer, options?: { mediaType?: string; }): Promise ``` - Converts the input ArrayBuffer to a Uint8Array for S3 compatibility - - Validates bucket configuration - - Uploads file with metadata (content type) - - Includes error handling and logging + - Invokes the `s3-upload` Edge Function to get a pre-signed URL to upload the file + ```typescript + const response = await this.client.functions.invoke('s3-upload', { + body: { + fileName: filename, + mediaType: options?.mediaType + } + }); + // error handling + const { uploadUrl } = response.data; + ``` + - Converts the input ArrayBuffer to an Uint8Array for S3 compatibility + ```typescript + const body = new Uint8Array(data); + ``` + - Uploads the file with metadata (content type) to the pre-signed upload URL + ```typescript + await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Length": body.length.toString(), + "Content-Type": options?.mediaType, + }, + body: body, + }); + ``` ```typescript async downloadFile(filePath: string): Promise ``` - - Creates a new S3 client instance with configured credentials - - Retrieves object from S3 - - Converts the response to a Blob for client-side usage - - Includes error handling for missing files/data + - Invokes the `s3-download` Edge Function to get a pre-signed URL to download the file + ```typescript + const response = await this.client.functions.invoke('s3-download', { + body: { + fileName: filePath + } + }); + // error handling + const { downloadUrl } = response.data; + ``` + - Fetch the file from S3 using the pre-signed URL and converts the response to a Blob for client-side usage + ```typescript + const downloadResponse = await fetch(downloadUrl, { + method: "GET", + }); + + return await downloadResponse.blob(); + ``` @@ -300,23 +499,27 @@ sidebarTitle: "AWS S3 Storage" async deleteFile(uri: string, options?: { filename?: string }): Promise ``` Two-step deletion process: - 1. Deletes local file if it exists (using Expo's FileSystem) - 2. Deletes remote file from S3 if filename is provided - - Includes validation and error handling + 1. Delete local file if it exists (using Expo's FileSystem) + 2. Delete remote file from S3 by invoking the `s3-delete` Edge Function + ```typescript + const response = await this.client.functions.invoke('s3-delete', { + body: { + fileName: options?.filename, + } + }); + ``` - + Update the `system.ts` file in the `demos/react-native-supabase-todolist/library/config` directory to use the new `AWSStorageAdapter` class (the highlighted lines are the only changes needed): - ```typescript system.ts {5-6, 13, 19, 27-34, 54} + ```typescript system.ts {12, 18, 26} import '@azure/core-asynciterator-polyfill'; import { PowerSyncDatabase } from '@powersync/react-native'; import React from 'react'; - import S3 from 'aws-sdk/clients/s3'; import { type AttachmentRecord } from '@powersync/attachments'; import Logger from 'js-logger'; import { KVStorage } from '../storage/KVStorage'; @@ -338,14 +541,7 @@ sidebarTitle: "AWS S3 Storage" constructor() { this.kvStorage = new KVStorage(); this.supabaseConnector = new SupabaseConnector(this); - const s3Client = new S3({ - region: AppConfig.region, - credentials: { - accessKeyId: AppConfig.accessKeyId, - secretAccessKey: AppConfig.secretAccessKey - } - }); - this.storage = new AWSStorageAdapter({ client: s3Client }); + this.storage = new AWSStorageAdapter(this.supabaseConnector.client); this.powersync = new PowerSyncDatabase({ schema: AppSchema, database: { @@ -365,7 +561,7 @@ sidebarTitle: "AWS S3 Storage" * this.powersync = new PowerSyncDatabase({ database: factory, schema: AppSchema }); */ - if (AppConfig.s3bucketName) { + if (AppConfig.supabaseBucket) { this.attachmentQueue = new PhotoAttachmentQueue({ powersync: this.powersync, storage: this.storage, @@ -399,71 +595,42 @@ sidebarTitle: "AWS S3 Storage" ``` - + + + Ensure that all references to`AppConfig.supabaseBucket` is replaced with the S3 bucket name in the [React Native To-Do List demo app](https://github.com/powersync-ja/powersync-js/tree/main/demos/react-native-supabase-todolist). + + Obtaining the S3 bucket name in the client can be done by creating another Supabsae Edge Function that returns the bucket name. This ensures that all S3 information are + kept on the server. + You can now run the app and test the attachment upload and download functionality. -## The complete files used in this tutorial can be found below: +## The complete client files used in this tutorial can be found below - ```bash .env - # Replace the credentials below with your Supabase, PowerSync and Expo project details. - EXPO_PUBLIC_SUPABASE_URL=https://foo.supabase.co - EXPO_PUBLIC_SUPABASE_ANON_KEY=foo - EXPO_PUBLIC_ATTACHMENT_STORAGE_OPTION=supabase # Change this to s3 to use AWS S3 storage for attachments - EXPO_PUBLIC_SUPABASE_BUCKET= # Optional. Only required when syncing attachments and using Supabase Storage. See packages/powersync-attachments. - EXPO_PUBLIC_POWERSYNC_URL=https://foo.powersync.journeyapps.com - EXPO_PUBLIC_EAS_PROJECT_ID=foo # Optional. Only required when using EAS. - EXPO_PUBLIC_AWS_S3_REGION=region - EXPO_PUBLIC_AWS_S3_BUCKET_NAME=bucket_name - EXPO_PUBLIC_AWS_S3_ACCESS_KEY_ID=*** - EXPO_PUBLIC_AWS_S3_ACCESS_SECRET_ACCESS_KEY=*** - ``` - ```typescript process-env.d.ts - export {}; - - declare global { - namespace NodeJS { - interface ProcessEnv { - [key: string]: string | undefined; - EXPO_PUBLIC_SUPABASE_URL: string; - EXPO_PUBLIC_SUPABASE_ANON_KEY: string; - EXPO_PUBLIC_SUPABASE_BUCKET: string; - EXPO_PUBLIC_POWERSYNC_URL: string; - EXPO_PUBLIC_EAS_PROJECT_ID: string; - EXPO_PUBLIC_AWS_S3_REGION: string; - EXPO_PUBLIC_AWS_S3_BUCKET_NAME: string; - EXPO_PUBLIC_AWS_S3_ACCESS_KEY_ID: string; - EXPO_PUBLIC_AWS_S3_ACCESS_SECRET_ACCESS_KEY: string; - } - } - } - ``` - ```typescript AppConfig.ts - export const AppConfig = { - supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL, - supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY, - supabaseBucket: process.env.EXPO_PUBLIC_SUPABASE_BUCKET || '', - powersyncUrl: process.env.EXPO_PUBLIC_POWERSYNC_URL, - region: process.env.EXPO_PUBLIC_AWS_S3_REGION, - accessKeyId: process.env.EXPO_PUBLIC_AWS_S3_ACCESS_KEY_ID || '', - secretAccessKey: process.env.EXPO_PUBLIC_AWS_S3_ACCESS_SECRET_ACCESS_KEY || '', - s3bucketName: process.env.EXPO_PUBLIC_AWS_S3_BUCKET_NAME || '' - }; - ``` ```typescript AWSStorageAdapter.ts import * as FileSystem from 'expo-file-system'; - import S3 from 'aws-sdk/clients/s3'; import { decode as decodeBase64 } from 'base64-arraybuffer'; import { StorageAdapter } from '@powersync/attachments'; import { AppConfig } from '../supabase/AppConfig'; - - export interface S3StorageAdapterOptions { - client: S3; + import { SupabaseClient } from '@supabase/supabase-js'; + + interface S3Upload { + message: string; + uploadUrl: string; + } + + interface S3Download { + message: string; + downloadUrl: string; + } + + interface S3Delete { + message: string; } export class AWSStorageAdapter implements StorageAdapter { - constructor(private options: S3StorageAdapterOptions) {} + constructor( public client: SupabaseClient ) {} async uploadFile( filename: string, @@ -472,21 +639,32 @@ sidebarTitle: "AWS S3 Storage" mediaType?: string; } ): Promise { - if (!AppConfig.s3bucketName) { - throw new Error('AWS S3 bucket not configured in AppConfig.ts'); + + const response = await this.client.functions.invoke('s3-upload', { + body: { + fileName: filename, + mediaType: options?.mediaType + } + }); + + if (response.error || !response.data) { + throw new Error(`Failed to reach upload edge function, code=${response.error}`); } + const { uploadUrl } = response.data; try { - const body = Uint8Array.from(new Uint8Array(data)); - const params = { - Bucket: AppConfig.s3bucketName, - Key: filename, - Body: body, - ContentType: options?.mediaType - }; - - await this.options.client.upload(params).promise(); - console.log(`File uploaded successfully to ${AppConfig.s3bucketName}/${filename}`); + const body = new Uint8Array(data); + + const response = await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Length": body.length.toString(), + "Content-Type": options?.mediaType, + }, + body: body, + }); + + console.log(`File: ${filename} uploaded successfully.`); } catch (error) { console.error('Error uploading file:', error); throw error; @@ -494,25 +672,24 @@ sidebarTitle: "AWS S3 Storage" } async downloadFile(filePath: string): Promise { - const s3 = new S3({ - region: AppConfig.region, - accessKeyId: AppConfig.accessKeyId, - secretAccessKey: AppConfig.secretAccessKey + const response = await this.client.functions.invoke('s3-download', { + body: { + fileName: filePath + } }); - const params = { - Bucket: AppConfig.s3bucketName, - Key: filePath - }; + if (response.error || !response.data) { + throw new Error(`Failed to reach download edge function, code=${response.error}`); + } + + const { downloadUrl } = response.data; try { - const obj = await s3.getObject(params).promise(); - if (obj.Body) { - const data = await new Response(obj.Body as ReadableStream).arrayBuffer(); - return new Blob([data]); - } else { - throw new Error('Object body is undefined. Could not download file.'); - } + const downloadResponse = await fetch(downloadUrl, { + method: "GET", + }); + + return await downloadResponse.blob(); } catch (error) { console.error('Error downloading file:', error); throw error; @@ -529,19 +706,21 @@ sidebarTitle: "AWS S3 Storage" return; } - if (!AppConfig.s3bucketName) { - throw new Error('Supabase bucket not configured in AppConfig.ts'); - } - try { - const params = { - Bucket: AppConfig.s3bucketName, - Key: filename - }; - await this.options.client.deleteObject(params).promise(); - console.log(`${filename} deleted successfully from ${AppConfig.s3bucketName}.`); + const response = await this.client.functions.invoke('s3-delete', { + body: { + fileName: options?.filename + } + }); + + if (response.error || !response.data) { + throw new Error(`Failed to reach delete edge function, code=${response.error}`); + } + + const { message } = response.data; + console.log(message); } catch (error) { - console.error(`Error deleting ${filename} from ${AppConfig.s3bucketName}:`, error); + console.error(`Error deleting ${filename}:`, error); } } @@ -552,7 +731,7 @@ sidebarTitle: "AWS S3 Storage" const { encoding = FileSystem.EncodingType.UTF8 } = options ?? {}; const { exists } = await FileSystem.getInfoAsync(fileURI); if (!exists) { - throw new Error(`File does not exist: ${fileURI}`); + throw new Error(`File does not exist: ${fileURI}`); } const fileContent = await FileSystem.readAsStringAsync(fileURI, options); if (encoding === FileSystem.EncodingType.Base64) { @@ -580,7 +759,7 @@ sidebarTitle: "AWS S3 Storage" async makeDir(uri: string): Promise { const { exists } = await FileSystem.getInfoAsync(uri); if (!exists) { - await FileSystem.makeDirectoryAsync(uri, { intermediates: true }); + await FileSystem.makeDirectoryAsync(uri, { intermediates: true }); } } @@ -610,7 +789,6 @@ sidebarTitle: "AWS S3 Storage" import { PowerSyncDatabase } from '@powersync/react-native'; import React from 'react'; - import S3 from 'aws-sdk/clients/s3'; import { type AttachmentRecord } from '@powersync/attachments'; import Logger from 'js-logger'; import { KVStorage } from '../storage/KVStorage'; @@ -632,14 +810,7 @@ sidebarTitle: "AWS S3 Storage" constructor() { this.kvStorage = new KVStorage(); this.supabaseConnector = new SupabaseConnector(this); - const s3Client = new S3({ - region: AppConfig.region, - credentials: { - accessKeyId: AppConfig.accessKeyId, - secretAccessKey: AppConfig.secretAccessKey - } - }); - this.storage = new AWSStorageAdapter({ client: s3Client }); + this.storage = new AWSStorageAdapter(this.supabaseConnector.client); this.powersync = new PowerSyncDatabase({ schema: AppSchema, database: { @@ -647,19 +818,19 @@ sidebarTitle: "AWS S3 Storage" } }); /** - * The snippet below uses OP-SQLite as the default database adapter. - * You will have to uninstall `@journeyapps/react-native-quick-sqlite` and - * install both `@powersync/op-sqlite` and `@op-engineering/op-sqlite` to use this. - * - * import { OPSqliteOpenFactory } from '@powersync/op-sqlite'; // Add this import - * - * const factory = new OPSqliteOpenFactory({ - * dbFilename: 'sqlite.db' - * }); - * this.powersync = new PowerSyncDatabase({ database: factory, schema: AppSchema }); - */ - - if (AppConfig.s3bucketName) { + * The snippet below uses OP-SQLite as the default database adapter. + * You will have to uninstall `@journeyapps/react-native-quick-sqlite` and + * install both `@powersync/op-sqlite` and `@op-engineering/op-sqlite` to use this. + * + * import { OPSqliteOpenFactory } from '@powersync/op-sqlite'; // Add this import + * + * const factory = new OPSqliteOpenFactory({ + * dbFilename: 'sqlite.db' + * }); + * this.powersync = new PowerSyncDatabase({ database: factory, schema: AppSchema }); + */ + + if (AppConfig.supabaseBucket) { this.attachmentQueue = new PhotoAttachmentQueue({ powersync: this.powersync, storage: this.storage, @@ -667,7 +838,7 @@ sidebarTitle: "AWS S3 Storage" // and/or the exception to decide if you want to retry the download onDownloadError: async (attachment: AttachmentRecord, exception: any) => { if (exception.toString() === 'StorageApiError: Object not found') { - return { retry: false }; + return { retry: false }; } return { retry: true };