Implementing S3 Image Uploads with React-Dropzone and .NET

/5 min read

A comprehensive guide to implementing secure image uploads to AWS S3 using React-Dropzone, Next.js, and .NET with Transfer Utility

Implementing Secure S3 Image Uploads with React-Dropzone and .NET#

Building a robust file upload system can be challenging, but with the right tools and architecture, we can create a secure and efficient solution. In this guide, we'll implement an image upload feature using React-Dropzone for the frontend and .NET with AWS Transfer Utility for handling the backend operations with AWS S3.

Frontend Implementation with Next.js and React-Dropzone#

First, let's set up our frontend component using React-Dropzone. We'll create a reusable upload component that handles drag-and-drop functionality and preview capabilities.

// components/ImageUpload.tsx
import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import Image from 'next/image';

interface ImageUploadProps {
  onUpload: (file: File) => Promise<void>;
}

export default function ImageUpload({ onUpload }: ImageUploadProps) {
  const [preview, setPreview] = useState<string | null>(null);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const onDrop = useCallback(async (acceptedFiles: File[]) => {
    try {
      const file = acceptedFiles[0];
      setUploading(true);
      setProgress(0);
      
      // Create preview
      const objectUrl = URL.createObjectURL(file);
      setPreview(objectUrl);
      
      await onUpload(file);
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
    }
  }, [onUpload]);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      'image/*': ['.jpeg', '.jpg', '.png', '.gif']
    },
    maxSize: 5242880, // 5MB
    multiple: false
  });

  return (
    <div className="w-full max-w-xl mx-auto">
      <div
        {...getRootProps()}
        className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer
          ${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}`}
      >
        <input {...getInputProps()} />
        {uploading ? (
          <div>
            <p>Uploading... {progress}%</p>
            <div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
              <div 
                className="bg-blue-600 h-2.5 rounded-full" 
                style={{ width: `${progress}%` }}
              ></div>
            </div>
          </div>
        ) : (
          <p>Drag and drop an image here, or click to select</p>
        )}
      </div>

      {preview && (
        <div className="mt-4">
          <Image
            src={preview}
            alt="Preview"
            width={400}
            height={300}
            className="rounded-lg"
          />
        </div>
      )}
    </div>
  );
}

Backend Implementation with .NET Transfer Utility#

For the backend, we'll create a clean architecture using AWS Transfer Utility for efficient file uploads. First, let's define our S3 service interface:

// Services/Interfaces/IS3Service.cs
public interface IS3Service
{
    Task<string> UploadImageAsync(IFormFile file, string prefix = "uploads", IProgress<int> progress = null);
    Task DeleteImageAsync(string key);
}

Now, let's implement the S3 service using Transfer Utility:

// Services/S3Service.cs
public class S3Service : IS3Service
{
    private readonly IAmazonS3 _s3Client;
    private readonly string _bucketName;
    private readonly TransferUtility _transferUtility;

    public S3Service(
        IAmazonS3 s3Client,
        IConfiguration configuration)
    {
        _s3Client = s3Client;
        _bucketName = configuration["AWS:BucketName"];
        _transferUtility = new TransferUtility(_s3Client);
    }

    public async Task<string> UploadImageAsync(IFormFile file, string prefix = "uploads", IProgress<int> progress = null)
    {
        try
        {
            // Generate unique file name
            var fileName = $"{prefix}/{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";

            // Create a temporary file
            var tempFile = Path.GetTempFileName();
            using (var stream = new FileStream(tempFile, FileMode.Create))
            {
                await file.CopyToAsync(stream);
            }

            // Configure upload request
            var fileTransferUtilityRequest = new TransferUtilityUploadRequest
            {
                FilePath = tempFile,
                BucketName = _bucketName,
                Key = fileName,
                ContentType = file.ContentType,
                CannedACL = S3CannedACL.PublicRead
            };

            // Add progress tracking
            if (progress != null)
            {
                fileTransferUtilityRequest.UploadProgressEvent += (sender, args) =>
                {
                    var percentDone = (int)((double)args.TransferredBytes * 100 / args.TotalBytes);
                    progress.Report(percentDone);
                };
            }

            // Upload file
            await _transferUtility.UploadAsync(fileTransferUtilityRequest);

            // Clean up temporary file
            File.Delete(tempFile);

            // Return the file URL
            return $"https://{_bucketName}.s3.amazonaws.com/{fileName}";
        }
        catch (Exception ex)
        {
            throw new ApplicationException("Error uploading to S3", ex);
        }
    }

    public async Task DeleteImageAsync(string key)
    {
        try
        {
            await _transferUtility.S3Client.DeleteObjectAsync(_bucketName, key);
        }
        catch (Exception ex)
        {
            throw new ApplicationException("Error deleting from S3", ex);
        }
    }
}

Now let's create our controller to handle the upload requests with progress tracking:

// Controllers/ImageController.cs
[ApiController]
[Route("api/[controller]")]
public class ImageController : ControllerBase
{
    private readonly IS3Service _s3Service;
    private readonly ILogger<ImageController> _logger;

    public ImageController(
        IS3Service s3Service,
        ILogger<ImageController> logger)
    {
        _s3Service = s3Service;
        _logger = logger;
    }

    [HttpPost("upload")]
    public async Task<IActionResult> UploadImage(IFormFile file)
    {
        try
        {
            if (file == null || file.Length == 0)
                return BadRequest("No file uploaded");

            // Validate file type
            var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/gif" };
            if (!allowedTypes.Contains(file.ContentType.ToLower()))
                return BadRequest("Invalid file type");

            // Validate file size (5MB max)
            if (file.Length > 5242880)
                return BadRequest("File size exceeds limit");

            var progress = new Progress<int>();
            var imageUrl = await _s3Service.UploadImageAsync(file, progress: progress);
            return Ok(new { url = imageUrl });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error uploading image");
            return StatusCode(500, "Internal server error");
        }
    }
}

Configuration and Setup#

To complete the implementation, make sure to:

  1. Install required NuGet packages:
<PackageReference Include="AWSSDK.S3" Version="3.7.300.0" />
<PackageReference Include="AWSSDK.TransferUtility" Version="3.7.300.0" />
  1. Configure AWS credentials in your appsettings.json:
{
  "AWS": {
    "BucketName": "your-bucket-name",
    "Region": "your-region"
  }
}
  1. Register services in Program.cs:
builder.Services.AddAWSService<IAmazonS3>();
builder.Services.AddScoped<IS3Service, S3Service>();

Advantages of Using Transfer Utility#

  1. Automatic Multipart Uploads: Transfer Utility automatically handles multipart uploads for large files, improving upload reliability and performance.
  2. Built-in Retry Logic: Provides automatic retry capability for failed uploads.
  3. Progress Tracking: Native support for upload progress monitoring.
  4. Pause and Resume: Supports pausing and resuming uploads (useful for large files).
  5. Concurrent Uploads: Handles multiple file uploads efficiently.

This implementation provides a robust foundation for handling image uploads in your application, with the added benefits of Transfer Utility's reliability and performance features. You can extend it further by implementing pause/resume functionality, adding support for multiple file uploads, or integrating image processing capabilities.

Loading comments…