-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: anmho/fix-api-dev-server
Are you sure you want to change the base?
Changes from 5 commits
d85c8e3
d348770
5403ca9
7d93855
ef64f79
121d908
4cae2db
e5cba1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)) | ||
|
@@ -94,6 +96,6 @@ func registerRoutes( | |
}, | ||
}, protectedGreetHandler()) | ||
|
||
huma.Get(api, "/create-upload-url", CreateUploadURLHandler(imageService)) | ||
huma.Post(api, "/create-upload-url", CreateUploadURLHandler(imageService)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Post since we are created a presigned upload url resource. |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,7 +63,7 @@ export default function Page() { | |
|
||
if (completeSignIn.status === "complete") { | ||
await setActive({ session: completeSignIn.createdSessionId }); | ||
router.replace("/(home)"); | ||
router.replace("/(tabs)"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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> | ||
|
This file was deleted.
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 |
---|---|---|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
} |
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", | ||
}, | ||
}); |
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"); | ||
} |
There was a problem hiding this comment.
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.