Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): upload images client flow with s3 presigned #67

Open
wants to merge 8 commits into
base: anmho/fix-api-dev-server
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ watch:
# Generates the client SDK from the server's current OpenAPI spec.
gen:
@$(MAKE) -C api openapi
@orval --input ./api/openapi.yaml --output ./client-v2/gen/openapi.ts
@orval --input ./api/openapi.yaml --output ./client-v2/lib/api/happened.ts
Copy link
Collaborator Author

@anmho anmho Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to a lib/api folder for to improve semantics.



3 changes: 2 additions & 1 deletion api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,13 @@ func main() {
}
logger.Info("successfully pinged db")

cfg, err := awsConfig.LoadDefaultConfig(ctx)
cfg, err := awsConfig.LoadDefaultConfig(ctx, awsConfig.WithRegion("us-west-2"))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force us-west-2 but this should be an environment variable.

if err != nil {
logger.Error("loading default aws config", slog.Any("error", err))
os.Exit(1)
}


// Setup S3 bucket
s3Client := s3.NewFromConfig(cfg)
s3PresignClient := s3.NewPresignClient(s3Client)
Expand Down
6 changes: 3 additions & 3 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ info:
openapi: 3.1.0
paths:
/create-upload-url:
get:
operationId: get-create-upload-url
post:
operationId: post-create-upload-url
requestBody:
content:
application/json:
Expand All @@ -149,7 +149,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Error
summary: Get create upload URL
summary: Post create upload URL
/greeting/protected/{name}:
get:
description: Protected version of greet
Expand Down
3 changes: 2 additions & 1 deletion api/pkg/images/image_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package images

import (
"context"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"log"
"time"

v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"

"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go/aws"
)
Expand Down
8 changes: 5 additions & 3 deletions api/pkg/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ package server
import (
"database/sql"
"fmt"
"github.com/clerk/clerk-sdk-go/v2/jwt"
"log/slog"
"net/http"
"strings"

"github.com/clerk/clerk-sdk-go/v2/jwt"

"github.com/danielgtaylor/huma/v2/adapters/humachi"

"happenedapi/pkg/images"

"github.com/danielgtaylor/huma/v2"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"happenedapi/pkg/images"
)

type HumaMiddleware func(ctx huma.Context, next func(huma.Context))
Expand Down Expand Up @@ -94,6 +96,6 @@ func registerRoutes(
},
}, protectedGreetHandler())

huma.Get(api, "/create-upload-url", CreateUploadURLHandler(imageService))
huma.Post(api, "/create-upload-url", CreateUploadURLHandler(imageService))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post since we are created a presigned upload url resource.


}
6 changes: 6 additions & 0 deletions client-v2/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
],
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let you share them with your friends."
}
]
],
"experiments": {
Expand Down
4 changes: 2 additions & 2 deletions client-v2/app/(auth)/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default function Page() {

if (completeSignIn.status === "complete") {
await setActive({ session: completeSignIn.createdSessionId });
router.replace("/(home)");
router.replace("/(tabs)");
Copy link
Collaborator Author

@anmho anmho Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Navigate to tabs layout as the home.

} else {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
Expand All @@ -79,7 +79,7 @@ export default function Page() {
<View className="h-full justify-center pb-16">
<View className="absolute left-4 top-8">
<TouchableHighlight>
<Link href="/(home)">
<Link href="/(tabs)">
<Ionicons name="chevron-back" size={32} color="black" />
</Link>
</TouchableHighlight>
Expand Down
4 changes: 2 additions & 2 deletions client-v2/app/(auth)/sign-up.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function SignUpScreen() {

if (completeSignUp.status === "complete") {
await setActive({ session: completeSignUp.createdSessionId });
router.replace("/(home)");
router.replace("/(tabs)");
} else {
console.error(JSON.stringify(completeSignUp, null, 2));
}
Expand All @@ -59,7 +59,7 @@ export default function SignUpScreen() {
<View className="h-full justify-center pb-16">
<View className="absolute left-4 top-8">
<Pressable>
<Link href="/(home)">
<Link href="/(tabs)">
<Ionicons name="chevron-back" size={32} color="black" />
</Link>
</Pressable>
Expand Down
5 changes: 0 additions & 5 deletions client-v2/app/(home)/_layout.tsx

This file was deleted.

29 changes: 29 additions & 0 deletions client-v2/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { BottomTabBarHeightCallbackContext } from "@react-navigation/bottom-tabs";
import { Tabs } from "expo-router";

export default function TabLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: "blue", headerShown: false }}>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<FontAwesome size={28} name="home" color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ color }) => (
<FontAwesome size={28} name="cog" color={color} />
),
}}
/>
<Tabs.Screen name="post" />
</Tabs>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getGreeting } from "@/gen/openapi";
import { getGreeting } from "@/lib/api/happened";
import { SignedIn, SignedOut, useClerk, useUser } from "@clerk/clerk-expo";
import axios, { AxiosError } from "axios";
import { Link } from "expo-router";
Expand Down
64 changes: 64 additions & 0 deletions client-v2/app/(tabs)/post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { SafeAreaView } from "react-native-safe-area-context";
import { Text, TouchableOpacity, Image } from "react-native";
import { useState } from "react";
import * as ImagePicker from "expo-image-picker";
import { ImagePickerAsset } from "expo-image-picker";

import { uploadImages } from "@/lib/api/upload";
import { useMutation } from "@tanstack/react-query";

export default function PostTab() {
const [images, setImages] = useState<ImagePickerAsset[]>([]);
// Access the client
// const queryClient = useQueryClient();

// Queries
const { isPending: isUploadPending, mutateAsync: uploadImagesMutation } =
useMutation({
mutationFn: uploadImages,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upload/create journey tab.

onSuccess: () => console.log("successfully uploaded images"),
onError: (e) => console.error(e),
});
// uploadImagesMutation.mutate

const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: false,
aspect: [4, 3],
quality: 1,
allowsMultipleSelection: true,
selectionLimit: 10,
exif: true,
});
// console.log(result);
if (!result.canceled) {
setImages(result.assets);
}
};

return (
<SafeAreaView className="flex items-center justify-center bg-white h-full w-full">
<Text>Hello Post Tab</Text>

<TouchableOpacity onPress={pickImage}>
<Text>Select Image for Upload</Text>
</TouchableOpacity>
{images && (
<Image
source={{ uri: images[0]?.uri }}
className="aspect-square w-1/2 h-1/2"
/>
)}

<TouchableOpacity
onPress={async () => {
uploadImagesMutation(images);
}}
>
<Text>Upload Image</Text>
</TouchableOpacity>
{isUploadPending && <Text>Uploading {images.length} images...</Text>}
</SafeAreaView>
);
}
17 changes: 17 additions & 0 deletions client-v2/app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { View, Text, StyleSheet } from "react-native";

export default function SettingsTab() {
return (
<View style={styles.container}>
<Text>Tab [Settings]</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
});
15 changes: 10 additions & 5 deletions client-v2/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Slot } from "expo-router";
import * as SecureStore from "expo-secure-store";
import { ClerkProvider, ClerkLoaded } from "@clerk/clerk-expo";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import "@/global.css";

const queryClient = new QueryClient();

export default function RootLayout() {
const tokenCache = {
async getToken(key: string) {
Expand Down Expand Up @@ -37,10 +40,12 @@ export default function RootLayout() {
}

return (
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded>
<Slot />
</ClerkLoaded>
</ClerkProvider>
<QueryClientProvider client={queryClient}>
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded>
<Slot />
</ClerkLoaded>
</ClerkProvider>
</QueryClientProvider>
);
}
2 changes: 1 addition & 1 deletion client-v2/components/sign-in-with-o-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const SignInWithOAuth = () => {
// Use signIn or signUp for next steps such as MFA
console.log("session created but no further action taken");
}
router.navigate("/(home)");
router.navigate("/(tabs)");
} catch (err) {
console.error("OAuth error", err);
}
Expand Down
11 changes: 6 additions & 5 deletions client-v2/gen/openapi.ts → client-v2/lib/api/happened.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,14 @@ export interface CreateUploadURLBody {


/**
* @summary Get create upload URL
* @summary Post create upload URL
*/
export const getCreateUploadUrl = <TData = AxiosResponse<CreateUploadURLBody>>(
export const postCreateUploadUrl = <TData = AxiosResponse<CreateUploadURLBody>>(
createUploadURLRequestBody: NonReadonly<CreateUploadURLRequestBody>, options?: AxiosRequestConfig
): Promise<TData> => {
return axios.get(
`/create-upload-url`,options
return axios.post(
`/create-upload-url`,
createUploadURLRequestBody,options
);
}

Expand Down Expand Up @@ -129,6 +130,6 @@ export const getGreeting = <TData = AxiosResponse<GreetingOutputBody>>(
);
}

export type GetCreateUploadUrlResult = AxiosResponse<CreateUploadURLBody>
export type PostCreateUploadUrlResult = AxiosResponse<CreateUploadURLBody>
export type ProtectedGreetResult = AxiosResponse<GreetingOutputBody>
export type GetGreetingResult = AxiosResponse<GreetingOutputBody>
49 changes: 49 additions & 0 deletions client-v2/lib/api/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import axios from "axios";
import * as FileSystem from "expo-file-system";
import { Buffer } from "buffer";
import * as Crypto from "expo-crypto";
import { postCreateUploadUrl } from "@/lib/api/happened";
import { ImagePickerAsset } from "expo-image-picker";
import { ImagePropsAndroid } from "react-native";

export async function uploadImage(image: ImagePickerAsset) {
const imageKey = Crypto.randomUUID();
const res = await postCreateUploadUrl({ image_key: imageKey }).catch(
console.error,
);
if (!res) {
console.error("no response from post create upload url");
return;
}

console.log("got response");
const { method, signed_headers, upload_url } = res.data;

const base64 = await FileSystem.readAsStringAsync(image.uri, {
encoding: FileSystem.EncodingType.Base64,
});
console.log("base64", base64.length);

try {
const bytes = new Uint8Array(Buffer.from(base64, "base64"));
console.log("uploading image");
const resp = await axios.put(upload_url, bytes, {
headers: signed_headers,
});

console.log("done uploading image", resp);
} catch (e) {
console.error(e);
}
}

export async function uploadImages(images: ImagePickerAsset[]) {
const promises = [];
for (const image of images) {
const uploadPromise = uploadImage(image);
promises.push(uploadPromise);
}

await Promise.all(promises);
console.log("finished uploading all images");
}
Loading
Loading