diff --git a/src/Navigation.js b/src/Navigation.js index f461c44..4939b03 100644 --- a/src/Navigation.js +++ b/src/Navigation.js @@ -10,14 +10,13 @@ import useLoggedIn from './hooks/useLoggedIn'; import SignUp from './screens/SignUp'; import Buckets from './screens/Buckets'; import Settings from './screens/Settings'; +import DirectoryDetails from './screens/DirectoryDetails'; const Stack = createNativeStackNavigator(); const Navigation = () => { const [isInitializing, user] = useLoggedIn(); - console.log('[user]', user); - if (isInitializing) { return Loading...; } @@ -35,27 +34,30 @@ const Navigation = () => { component={SignUp} options={{title: 'Sign Up'}} /> - {!!user ? ( - - ({ - title: 'S3 Buckets', - headerRight: () => ( - navigation.navigate('Settings')}> - - - ), - })} - /> - - - ) : null} + ({ + title: 'S3 Buckets', + headerRight: () => ( + navigation.navigate('Settings')}> + + + ), + })} + /> + ({ + title: route.params.dir, + })} + /> + ); diff --git a/src/components/Bucket.js b/src/components/Bucket.js new file mode 100644 index 0000000..e61e7ac --- /dev/null +++ b/src/components/Bucket.js @@ -0,0 +1,41 @@ +import React, {useCallback} from 'react'; +import {StyleSheet, Text, Pressable} from 'react-native'; +import {useNavigation} from '@react-navigation/native'; + +const Bucket = ({bucket}) => { + const navigation = useNavigation(); + + const onPress = useCallback(() => { + navigation.navigate('DirectoryDetails', {bucket: bucket.Name, dir: '/'}); + }, [navigation, bucket]); + + return ( + + {bucket.Name} + + Created at: {bucket.CreationDate.toISOString()} + + + ); +}; + +const styles = StyleSheet.create({ + root: { + margin: 4, + backgroundColor: '#CCCCCC', + borderRadius: 5, + padding: 12, + }, + name: { + fontSize: 16, + fontWeight: 'bold', + marginBottom: 8, + color: '#000000', + }, + createdAt: { + fontSize: 12, + color: '#444444', + }, +}); + +export default Bucket; diff --git a/src/components/Directory.js b/src/components/Directory.js new file mode 100644 index 0000000..32de0c9 --- /dev/null +++ b/src/components/Directory.js @@ -0,0 +1,39 @@ +import React, {useCallback} from 'react'; +import {StyleSheet, Text, Pressable} from 'react-native'; +import {useNavigation} from '@react-navigation/native'; + +const Directory = ({bucket, dir}) => { + const navigation = useNavigation(); + + const onPress = useCallback(() => { + navigation.push('DirectoryDetails', { + bucket: bucket, + dir: dir.Prefix, + }); + }, [navigation, bucket]); + + const slashPos = dir.Prefix.lastIndexOf('/', dir.Prefix.length - 2); + + return ( + + + {slashPos > 0 ? dir.Prefix.slice(slashPos + 1) : dir.Prefix} + + + ); +}; + +const styles = StyleSheet.create({ + root: { + margin: 4, + backgroundColor: '#CCCCCC', + borderRadius: 5, + padding: 12, + }, + name: { + fontSize: 14, + color: '#000000', + }, +}); + +export default Directory; diff --git a/src/components/File.js b/src/components/File.js new file mode 100644 index 0000000..28bf638 --- /dev/null +++ b/src/components/File.js @@ -0,0 +1,39 @@ +import React, {useCallback} from 'react'; +import {StyleSheet, Text, Pressable} from 'react-native'; +import {useNavigation} from '@react-navigation/native'; + +const File = ({bucket, file}) => { + const navigation = useNavigation(); + + const onPress = useCallback(() => { + navigation.push('FileDetails', { + bucket: bucket, + file: file.Key, + }); + }, [navigation, bucket]); + + const slashPos = file.Key.lastIndexOf('/', file.Key.length - 2); + + return ( + + + {slashPos > 0 ? file.Key.slice(slashPos + 1) : file.Key} + + + ); +}; + +const styles = StyleSheet.create({ + root: { + margin: 4, + backgroundColor: '#CCCCCC', + borderRadius: 5, + padding: 12, + }, + name: { + fontSize: 14, + color: '#000000', + }, +}); + +export default File; diff --git a/src/hooks/useS3.js b/src/hooks/useS3.js new file mode 100644 index 0000000..d79b341 --- /dev/null +++ b/src/hooks/useS3.js @@ -0,0 +1,34 @@ +import {useEffect, useState} from 'react'; +import S3 from 'aws-sdk/clients/s3'; +import useSettings from './useSettings'; + +const useS3 = () => { + const [ + loaded, + _save, + awsKeyId, + _setAwsKeyId, + awsSecretAccessKey, + _setAwsSecretAccessKey, + ] = useSettings(); + const [s3, setS3] = useState(null); + + useEffect(() => { + if (loaded && !s3) { + setS3( + new S3({ + apiVersion: '2006-03-01', + region: 'eu-west-1', + credentials: { + accessKeyId: awsKeyId, + secretAccessKey: awsSecretAccessKey, + }, + }), + ); + } + }, [loaded]); + + return s3; +}; + +export default useS3; diff --git a/src/hooks/useSettings.js b/src/hooks/useSettings.js new file mode 100644 index 0000000..0bd4e00 --- /dev/null +++ b/src/hooks/useSettings.js @@ -0,0 +1,60 @@ +import {useState, useCallback, useEffect} from 'react'; +import {ToastAndroid} from 'react-native'; +import firestore from '@react-native-firebase/firestore'; +import useLoggedIn from './useLoggedIn'; + +const useSettings = () => { + const [ready, user] = useLoggedIn(); + const [awsKeyId, setAwsKeyId] = useState(''); + const [awsSecretAccessKey, setAwsSecretAccessKey] = useState(''); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + if (!isLoaded && user) { + firestore() + .collection('user-settings') + .doc(user.uid) + .get() + .then(doc => { + if (doc) { + const data = doc.data(); + setAwsKeyId(data['aws.keyId']); + setAwsSecretAccessKey(data['aws.secretAccessKey']); + } + }) + .catch(() => {}) + .finally(() => { + setIsLoaded(true); + }); + } + }, [user]); + + const save = useCallback(() => { + return firestore() + .collection('user-settings') + .doc(user.uid) + .set({ + 'aws.keyId': awsKeyId, + 'aws.secretAccessKey': awsSecretAccessKey, + }) + .then(() => { + ToastAndroid.show('Successfully saved settings', ToastAndroid.SHORT); + }) + .catch(e => { + console.log(e); + ToastAndroid.show('Failed to save settings', ToastAndroid.SHORT); + throw e; + }); + }, [user, awsKeyId, awsSecretAccessKey]); + + return [ + isLoaded, + save, + awsKeyId, + setAwsKeyId, + awsSecretAccessKey, + setAwsSecretAccessKey, + ]; +}; + +export default useSettings; diff --git a/src/screens/Buckets.js b/src/screens/Buckets.js index e89a72f..10ed579 100644 --- a/src/screens/Buckets.js +++ b/src/screens/Buckets.js @@ -1,10 +1,32 @@ -import React from 'react'; -import {StyleSheet, View, Text, ScrollView} from 'react-native'; +import React, {useState, useEffect} from 'react'; +import {ScrollView, ToastAndroid} from 'react-native'; +import useS3 from '../hooks/useS3'; +import Bucket from '../components/Bucket'; const Buckets = () => { - return ( - Signed in!!! - ) -} + const s3 = useS3(); + const [buckets, setBuckets] = useState([]); -export default Buckets; \ No newline at end of file + useEffect(() => { + if (s3 && !buckets.length) { + s3.listBuckets({}, (err, data) => { + if (err) { + ToastAndroid.show('Failed to fetch buckets', ToastAndroid.SHORT); + return; + } + + setBuckets(data.Buckets); + }); + } + }, [s3]); + + return ( + + {buckets.map(bucket => ( + + ))} + + ); +}; + +export default Buckets; diff --git a/src/screens/DirectoryDetails.js b/src/screens/DirectoryDetails.js new file mode 100644 index 0000000..a596389 --- /dev/null +++ b/src/screens/DirectoryDetails.js @@ -0,0 +1,58 @@ +import React, {useState, useEffect} from 'react'; +import {ScrollView, ToastAndroid} from 'react-native'; +import {useRoute} from '@react-navigation/native'; +import useS3 from '../hooks/useS3'; +import Directory from '../components/Directory'; +import File from '../components/File'; + +const DirectoryDetails = () => { + const route = useRoute(); + const s3 = useS3(); + const [contents, setContents] = useState({dirs: [], files: []}); + + useEffect(() => { + if (s3 && !contents.length) { + s3.listObjects( + { + Bucket: route.params.bucket, + Prefix: route.params.dir == '/' ? '' : route.params.dir, + Delimiter: '/', + MaxKeys: 5000, + }, + (err, data) => { + if (err) { + console.log(err); + ToastAndroid.show( + 'Failed to fetch directory contents', + ToastAndroid.SHORT, + ); + return; + } + + setContents({dirs: data.CommonPrefixes, files: data.Contents}); + }, + ); + } + }, [s3]); + + return ( + + {contents.dirs.map(dir => ( + + ))} + {contents.files.map(file => ( + + ))} + + ); +}; + +export default DirectoryDetails; diff --git a/src/screens/Settings.js b/src/screens/Settings.js index 690b22f..ee5f542 100644 --- a/src/screens/Settings.js +++ b/src/screens/Settings.js @@ -3,10 +3,19 @@ import {StyleSheet, View, Text, Button, TextInput} from 'react-native'; import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import useLogout from '../hooks/useLogout'; import {useNavigation} from '@react-navigation/native'; +import useSettings from '../hooks/useSettings'; const Settings = () => { const logOut = useLogout(); const navigation = useNavigation(); + const [ + isLoaded, + save, + awsKeyId, + setAwsKeyId, + awsSecretAccessKey, + setAwsSecretAccessKey, + ] = useSettings(); const onLogout = useCallback(() => { logOut().then(() => { @@ -14,9 +23,32 @@ const Settings = () => { }); }, [logOut]); + const onSave = useCallback(() => { + save().then(() => navigation.goBack()); + }, [save, navigation]); + + if (!isLoaded) { + return Loading...; + } + return ( + AWS Key ID + + AWS Secret Access Key + +