Uploading Files to Amazon S3 Using Presigned URLs in Flutter: A Step-by-Step Guide

Arjun R.
8 min readMay 11, 2024

--

Image

In this article, we’ll explore how to upload image files to Amazon S3 using presigned URLs in a Flutter application. Presigned URLs offer a secure way to upload files directly to S3 without the need for server-side code.

1. Understanding Presigned URLs:

Presigned URLs are URLs that grant temporary access to perform specific actions on an S3 object, such as uploading a file.

These URLs are generated by AWS services like the Amazon API Gateway and provide time-limited access with specified permissions.

2. Sending a GET Request to Amazon API Gateway:

Start by sending a GET request to the Amazon API Gateway, which will provide us with a presigned URL for uploading our file.

This request typically includes any necessary authentication tokens and parameters required by the API.

3. Uploading the File to S3 Using PUT Call:

Once we receive the presigned URL from the API Gateway, we use it to perform a PUT request.

The PUT request includes the image file as the request body and uploads it directly to the specified S3 bucket and key.

4. Handling Response and Obtaining S3 Object URL:

After a successful PUT request, we receive an HTTP 200 OK response, indicating that the file has been uploaded to S3.

Extract the S3 object URL from the response or construct it using the bucket and key information.

5. Making the Final POST Call:

With the S3 object URL obtained, include it in the data for the final POST call to our API endpoint.

Along with the URL, include any other required data such as title, description, and authorization token in the request headers.

Note: The API I’m going to use accepts a title, a description, and an image (thumb image) in its body along with an Authorization Token. Additionally, I’ll be replacing the API and Authorization Token with dummy text.

Let’s get started:

Step 1: Let’s create a layout to input a title, description, and an image.

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';

class UploadFileToS3 extends StatefulWidget {
const UploadFileToS3({super.key});

@override
State<UploadFileToS3> createState() => _UploadFileToS3State();
}

class _UploadFileToS3State extends State<UploadFileToS3> {
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();

File? _image;

final ImagePicker _picker = ImagePicker();

// Select the Image
Future<void> _selectImage() async {
final XFile? pickedFile =
await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
});

print('filepath: ${pickedFile.path}');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Upload File to S3'),
backgroundColor: Theme.of(context).primaryColorLight,
foregroundColor: Colors.black,
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: ListView(
children: [
const SizedBox(
height: 30,
),
const Text(
'Enter a title and select a file to upload',
style: TextStyle(
color: Colors.blue,
),
),
const SizedBox(
height: 30,
),

TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Enter a title',
border: OutlineInputBorder(),
),
),

const SizedBox(
height: 30,
),

Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
),
child: TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
contentPadding:
EdgeInsets.symmetric(horizontal: 10.0, vertical: 15.0),
),
maxLines: 10,
),
),

const SizedBox(
height: 30.0,
),

Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Select Image',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 8,
),
GestureDetector(
onTap: _selectImage,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
),
borderRadius: BorderRadius.circular(5.0),
),
child: _image != null
? Image.file(
_image!,
width: 200,
height: 100,
fit: BoxFit.cover,
)
: const Icon(Icons.add),
),
),
],
),

const SizedBox(
height: 40,
),

// Button to submit the data
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
Colors.blue,
),
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.all(16),
),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
onPressed: () {},
child: const Text(
'Submit',
style: TextStyle(
color: Colors.white,
),
),
),
],
),
),
);
}
}

It looks like this (you may use your own style):

Form Input Image

Here, I’m utilizing TextEditingController() for text and description input, and ImagePicker() for selecting an image from the gallery. Additionally, I’m displaying the selected image preview inside the container to the right of “Select Image”.

Step 2: Now, let’s add the code to retrieve the URL for uploading the file via a GET request.

// Get Presigned URL for the file we are supposed to upload (here image file).
//My api requires fileType and fileName. So, I've passed those in the data in post call.
Future<String?> _getPresignedUrl(File file) async {
try {
String fileType = file.path.endsWith('.png') ||
file.path.endsWith('.jpg') ||
file.path.endsWith('.jpeg')
? 'image'
: 'video';

print('Filetype: $fileType');

String fileName = 'file.${file.path.split('.').last}';

Response presignedUrlResponse = await Dio().post(
'Your getPresignedUrl',
data: {
'fileType': fileType,
'fileName': fileName,
},
options: Options(headers: {
'Authorization':
"Auth Token "
}),
);

return presignedUrlResponse.data['body-json']['body'];
} catch (error) {
print('Error getting presigned url: $error');
return null;
}
}

Step 3: Let’s add the code to make a PUT call to upload that file to that Presigned URL received.

// Now, uploading that file to the received presigned url
Future<String?> _uploadFileToPresignedUrl(
File file, String presignedUrl) async {
try {
List<int> fileBytes = await file.readAsBytes();

var request = http.Request('PUT', Uri.parse(presignedUrl));

String contentTypeString = file.path.endsWith('.png') ||
file.path.endsWith('.jpg') ||
file.path.endsWith('.jpeg')
? 'image'
: 'video';

// Send request headers
request.headers['Content-Type'] = contentTypeString;

// Send file bytes
request.bodyBytes = fileBytes;

// Send the request
var response = await request.send();

if (response.statusCode == 200) {
print('File uploaded successfully');
return presignedUrl.split('?').first;
} else {
print('Failed to upload the file: ${response.reasonPhrase}');
return null;
}
} catch (error) {
print('Error uploading file: $error');
return null;
}
}

Step 4: Now, let’s implement the method for the Submit button, where we’ll invoke the getPresignedUrl and uploadFileToPresignedUrl methods to achieve a complete and successful upload.

// Submitting the title, description and image file
Future<void> _submit() async {
String title = _titleController.text;
String description = _descriptionController.text;

try {
String? presignedUrl = await _getPresignedUrl(_image!);

if (presignedUrl != null) {
// Just to check what presignedURL we have received
print('PresignedURL Received: $presignedUrl');

String? uploadedUrl =
await _uploadFileToPresignedUrl(_image!, presignedUrl);

if (uploadedUrl != null) {
Response response = await Dio().post(
'Your API for posting data',
data: {
"title": title,
"description": description,
"thumbImage": uploadedUrl,
},
options: Options(headers: {
'Authorization':
"Auth Token"
}));

print('response on submit: $response');

setState(() {
_titleController.clear();
_descriptionController.clear();
_image = null;

// After Successful upload, showing a dialog
showDialog(
context: context,
builder: ((context) {
return AlertDialog(
title: const Text("File Uploaded Successfully"),
content: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
size: 100,
color: Colors.green,
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Okay"),
),
],
);
}),
);
});
} else {
print('Failed to Submit data');
}
} else {
print('Failed to get presigned URL');
}
} catch (error) {
print('Error submitting data: $error');
}
}

The final code will look like this:

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;

class UploadFileToS3 extends StatefulWidget {
const UploadFileToS3({super.key});

@override
State<UploadFileToS3> createState() => _UploadFileToS3State();
}

class _UploadFileToS3State extends State<UploadFileToS3> {
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();

File? _image;

final ImagePicker _picker = ImagePicker();

// Select the Image
Future<void> _selectImage() async {
final XFile? pickedFile =
await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_image = File(pickedFile.path);
});

print('filepath: ${pickedFile.path}');
}
}

// Get Presigned URL for the file we are supposed to upload (here image file).
//My api requires fileType and fileName. So, I've passed those in the data in post call.
Future<String?> _getPresignedUrl(File file) async {
try {
String fileType = file.path.endsWith('.png') ||
file.path.endsWith('.jpg') ||
file.path.endsWith('.jpeg')
? 'image'
: 'video';

print('Filetype: $fileType');

String fileName = 'file.${file.path.split('.').last}';

Response presignedUrlResponse = await Dio().post(
'Your getPresignedUrl',
data: {
'fileType': fileType,
'fileName': fileName,
},
options: Options(headers: {
'Authorization':
"Auth Token"
}),
);

return presignedUrlResponse.data['body-json']['body'];
} catch (error) {
print('Error getting presigned url: $error');
return null;
}
}

// Now, uploading that file to the received presigned url
Future<String?> _uploadFileToPresignedUrl(
File file, String presignedUrl) async {
try {
List<int> fileBytes = await file.readAsBytes();

var request = http.Request('PUT', Uri.parse(presignedUrl));

String contentTypeString = file.path.endsWith('.png') ||
file.path.endsWith('.jpg') ||
file.path.endsWith('.jpeg')
? 'image'
: 'video';

// Send request headers
request.headers['Content-Type'] = contentTypeString;

// Send file bytes
request.bodyBytes = fileBytes;

// Send the request
var response = await request.send();

if (response.statusCode == 200) {
print('File uploaded successfully');
return presignedUrl.split('?').first;
} else {
print('Failed to upload the file: ${response.reasonPhrase}');
return null;
}
} catch (error) {
print('Error uploading file: $error');
return null;
}
}

// Submitting the title, description and image file
Future<void> _submit() async {
String title = _titleController.text;
String description = _descriptionController.text;

try {
String? presignedUrl = await _getPresignedUrl(_image!);

if (presignedUrl != null) {
// Just to check what presignedURL we have received
print('PresignedURL Received: $presignedUrl');

String? uploadedUrl =
await _uploadFileToPresignedUrl(_image!, presignedUrl);

if (uploadedUrl != null) {
Response response = await Dio().post(
'Your API for posting data',
data: {
"title": title,
"description": description,
"thumbImage": uploadedUrl,
},
options: Options(headers: {
'Authorization':
"Auth Token"
}));

print('response on submit: $response');

setState(() {
_titleController.clear();
_descriptionController.clear();
_image = null;

// After Successful upload, showing a dialog
showDialog(
context: context,
builder: ((context) {
return AlertDialog(
title: const Text("File Uploaded Successfully"),
content: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle,
size: 100,
color: Colors.green,
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Okay"),
),
],
);
}),
);
});
} else {
print('Failed to Submit data');
}
} else {
print('Failed to get presigned URL');
}
} catch (error) {
print('Error submitting data: $error');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Upload File to S3'),
backgroundColor: Theme.of(context).primaryColorLight,
foregroundColor: Colors.black,
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: ListView(
children: [
const SizedBox(
height: 30,
),
const Text(
'Enter a title and select a file to upload',
style: TextStyle(
color: Colors.blue,
),
),
const SizedBox(
height: 30,
),

TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Enter a title',
border: OutlineInputBorder(),
),
),

const SizedBox(
height: 30,
),

Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
),
child: TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
contentPadding:
EdgeInsets.symmetric(horizontal: 10.0, vertical: 15.0),
),
maxLines: 10,
),
),

const SizedBox(
height: 30.0,
),

Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Select Image',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
width: 8,
),
GestureDetector(
onTap: _selectImage,
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
),
borderRadius: BorderRadius.circular(5.0),
),
child: _image != null
? Image.file(
_image!,
width: 200,
height: 100,
fit: BoxFit.cover,
)
: const Icon(Icons.add),
),
),
],
),

const SizedBox(
height: 40,
),

// Button to submit the data
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
Colors.blue,
),
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.all(16),
),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
onPressed: _submit,
child: const Text(
'Submit',
style: TextStyle(
color: Colors.white,
),
),
),
],
),
),
);
}
}

And we’re done.

You may check out the URL log in the terminal like this:

Log Image To Check URL

The highlighted URL is where the file is located. You can copy it and paste it into the browser to check, like this:

File Check

This GIF demonstrates how the uploading process will appear:

GIF to Show the File Upload

Happy Coding, Folks!

--

--