How can I upload a photo with Expo?
I'm making an app with Expo and want to let the user take a photo or pick one from their camera roll and upload it to my server. How do I do this?
Solution 1:
Use the Expo ImagePicker
API to display either the camera or the camera roll and get back information about the selected image:
async function takeAndUploadPhotoAsync() {
// Display the camera to the user and wait for them to take a photo or to cancel
// the action
let result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
});
if (result.cancelled) {
return;
}
// ImagePicker saves the taken photo to disk and returns a local URI to it
let localUri = result.uri;
let filename = localUri.split('/').pop();
// Infer the type of the image
let match = /\.(\w+)$/.exec(filename);
let type = match ? `image/${match[1]}` : `image`;
// Upload the image using the fetch and FormData APIs
let formData = new FormData();
// Assume "photo" is the name of the form field the server expects
formData.append('photo', { uri: localUri, name: filename, type });
return await fetch(YOUR_SERVER_URL, {
method: 'POST',
body: formData,
headers: {
'content-type': 'multipart/form-data',
},
});
}
For a more comprehensive example including the server code, see this repo: https://github.com/exponent/image-upload-example.
Solution 2:
The official examples use Node.js, here's how with PHP:
Expo
async function takePhotoAndUpload() {
let result = await ImagePicker.launchCameraAsync({
allowsEditing: false, // higher res on iOS
aspect: [4, 3],
});
if (result.cancelled) {
return;
}
let localUri = result.uri;
let filename = localUri.split('/').pop();
let match = /\.(\w+)$/.exec(filename);
let type = match ? `image/${match[1]}` : `image`;
let formData = new FormData();
formData.append('photo', { uri: localUri, name: filename, type });
return await fetch('http://example.com/upload.php', {
method: 'POST',
body: formData,
header: {
'content-type': 'multipart/form-data',
},
});
}
upload.php
<?php
move_uploaded_file($_FILES['photo']['tmp_name'], './photos/' . $_FILES['photo']['name']);
?>
Solution 3:
import React, { Component } from 'react';
import {
ActivityIndicator,
Button,
Clipboard,
Image,
Share,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { Constants } from 'expo';
import * as Permissions from 'expo-permissions';
import * as ImagePicker from 'expo-image-picker';
export default class App extends Component {
state = {
image: null,
uploading: false,
};
render() {
let {
image
} = this.state;
return (
<View style={styles.container}>
<StatusBar barStyle="default" />
<Text
style={styles.exampleText}>
Example: Upload ImagePicker result
</Text>
<Button
onPress={this._pickImage}
title="Pick an image from camera roll"
/>
<Button onPress={this._takePhoto} title="Take a photo" />
{this._maybeRenderImage()}
{this._maybeRenderUploadingOverlay()}
</View>
);
}
_maybeRenderUploadingOverlay = () => {
if (this.state.uploading) {
return (
<View
style={[StyleSheet.absoluteFill, styles.maybeRenderUploading]}>
<ActivityIndicator color="#fff" size="large" />
</View>
);
}
};
_maybeRenderImage = () => {
let {
image
} = this.state;
if (!image) {
return;
}
return (
<View
style={styles.maybeRenderContainer}>
<View
style={styles.maybeRenderImageContainer}>
<Image source={{ uri: image }} style={styles.maybeRenderImage} />
</View>
<Text
onPress={this._copyToClipboard}
onLongPress={this._share}
style={styles.maybeRenderImageText}>
{image}
</Text>
</View>
);
};
_share = () => {
Share.share({
message: this.state.image,
title: 'Check out this photo',
url: this.state.image,
});
};
_copyToClipboard = () => {
Clipboard.setString(this.state.image);
alert('Copied image URL to clipboard');
};
_takePhoto = async () => {
const {
status: cameraPerm
} = await Permissions.askAsync(Permissions.CAMERA);
const {
status: cameraRollPerm
} = await Permissions.askAsync(Permissions.CAMERA_ROLL);
// only if user allows permission to camera AND camera roll
if (cameraPerm === 'granted' && cameraRollPerm === 'granted') {
let pickerResult = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
});
if (!pickerResult.cancelled) {
this.setState({ image: pickerResult.uri });
}
this.uploadImageAsync(pickerResult.uri);
}
};
_pickImage = async () => {
const {
status: cameraRollPerm
} = await Permissions.askAsync(Permissions.CAMERA_ROLL);
// only if user allows permission to camera roll
if (cameraRollPerm === 'granted') {
let pickerResult = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
base64: true,
aspect: [4, 3],
});
if (!pickerResult.cancelled) {
this.setState({ image: pickerResult.uri});
}
this.uploadImageAsync(pickerResult.uri);
}
};
uploadImageAsync(pictureuri) {
let apiUrl = 'http://123.123.123.123/ABC';
var data = new FormData();
data.append('file', {
uri: pictureuri,
name: 'file',
type: 'image/jpg'
})
fetch(apiUrl, {
headers: {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data'
},
method: 'POST',
body: data
}).then(
response => {
console.log('succ ')
console.log(response)
}
).catch(err => {
console.log('err ')
console.log(err)
} )
}
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
exampleText: {
fontSize: 20,
marginBottom: 20,
marginHorizontal: 15,
textAlign: 'center',
},
maybeRenderUploading: {
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
},
maybeRenderContainer: {
borderRadius: 3,
elevation: 2,
marginTop: 30,
shadowColor: 'rgba(0,0,0,1)',
shadowOpacity: 0.2,
shadowOffset: {
height: 4,
width: 4,
},
shadowRadius: 5,
width: 250,
},
maybeRenderImageContainer: {
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
overflow: 'hidden',
},
maybeRenderImage: {
height: 250,
width: 250,
},
maybeRenderImageText: {
paddingHorizontal: 10,
paddingVertical: 10,
}
});
Solution 4:
Since the chosen solution didn't actually work for me, here's how I made file uploads work with Expo and Django Rest Framework as backend.
const blobToBase64 = blob => {
const reader = new FileReader();
reader.readAsDataURL(blob);
return new Promise(resolve => {
reader.onloadend = () => {
resolve(reader.result);
};
});
};
const formData = new FormData();
const base64 = await blobToBase64(blob);
formData.append('file', base64);
formData.append('data', JSON.stringify(payload)); // additional data (I parse the string as json in the backend to get my payload back)
// same code as chosen answer, this was not part of the problem
return await fetch(YOUR_SERVER_URL, {
method: 'POST',
body: formData,
headers: {
'content-type': 'multipart/form-data',
},
});
In Django, I can use a custom parser to decode the base64 string to bytes and then create a SimpleUploadedFile
object with it.
class MultipartJsonParser(parsers.MultiPartParser):
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream,
media_type=media_type,
parser_context=parser_context
)
base64_file = result.data.get('file')
file_parts = base64_file.split(',')
mime_type = re.sub(r'^data:([\w\/]+);base64$', '\\1', file_parts[0])
file = SimpleUploadedFile('file', base64.b64decode(file_parts[1]), mime_type)
data = json.loads(result.data["data"]) or {} # additional data sent by Expo app
qdict = QueryDict('', mutable=True)
qdict.update(data)
return parsers.DataAndFiles(qdict, {'file': file})
class MyUploadView(ModelViewSet):
parser_classes = (MultipartJsonParser, parsers.JSONParser)
def create(self, request, *args, **kwargs):
# request.data should have a 'file' property with a SimpleUploadedFile object
...
Solution 5:
For Expo, nothing worked for me except using the Expo FileSystem uploadAsync
uploadImage = async ({ imageUri } }) => FileSystem.uploadAsync(
apiUrl,
imageUri,
{
headers: {
// Auth etc
},
uploadType: FileSystem.FileSystemUploadType.MULTIPART,
fieldName: 'files',
mimeType: 'image/png',
});
Note - imageUri in format of file:///mypath/to/image.png