File Upload and Download with Spring Boot and Kotlin

December 21, 2024 3 min read Intermediate

In this tutorial, we'll create a complete file upload and download system using Spring Boot and Kotlin. We'll cover:

  • Setting up the file storage service
  • Creating REST endpoints for upload and download
  • Handling multiple file uploads
  • Implementing progress tracking
  • Adding basic validation
  • Error handling

Project Setup

First, ensure you have these dependencies in your build.gradle.kts:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}

File Storage Configuration

First, let's create a configuration class for file storage properties:

@Configuration
@ConfigurationProperties(prefix = "file")
class FileStorageProperties {
    lateinit var uploadDir: String
}

Add this to your application.properties:

file.upload-dir=./uploads
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

File Storage Service

Create a service to handle file operations:

@Service
class FileStorageService(
    private val fileStorageProperties: FileStorageProperties
) {
    private val root: Path

    init {
        root = Paths.get(fileStorageProperties.uploadDir)
            .toAbsolutePath()
            .normalize()
        
        Files.createDirectories(root)
    }

    fun storeFile(file: MultipartFile): String {
        // Normalize file name
        val fileName = StringUtils.cleanPath(file.originalFilename ?: throw IllegalArgumentException("Original filename must not be null"))

        try {
            // Check if the file name contains invalid characters
            if (fileName.contains("..")) {
                throw StorageException("Sorry! Filename contains invalid path sequence $fileName")
            }

            // Copy file to the target location
            val targetLocation = root.resolve(fileName)
            Files.copy(file.inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING)

            return fileName
        } catch (ex: IOException) {
            throw StorageException("Could not store file $fileName. Please try again!", ex)
        }
    }

    fun loadFileAsResource(fileName: String): Resource {
        try {
            val filePath = root.resolve(fileName).normalize()
            val resource = UrlResource(filePath.toUri())

            if (resource.exists()) {
                return resource
            } else {
                throw FileNotFoundException("File not found $fileName")
            }
        } catch (ex: MalformedURLException) {
            throw FileNotFoundException("File not found $fileName")
        }
    }
}

Exception Classes

Create custom exceptions:

class StorageException : RuntimeException {
    constructor(message: String) : super(message)
    constructor(message: String, cause: Throwable) : super(message, cause)
}

class FileNotFoundException(message: String) : RuntimeException(message)

Controller

Create a controller to handle upload and download requests:

@RestController
@RequestMapping("/api/files")
class FileController(
    private val fileStorageService: FileStorageService
) {
    @PostMapping("/upload")
    fun uploadFile(@RequestParam("file") file: MultipartFile): UploadFileResponse {
        val fileName = fileStorageService.storeFile(file)
        val fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
            .path("/api/files/download/")
            .path(fileName)
            .toUriString()

        return UploadFileResponse(
            fileName = fileName,
            fileDownloadUri = fileDownloadUri,
            fileType = file.contentType ?: "unknown",
            size = file.size
        )
    }

    @PostMapping("/upload-multiple")
    fun uploadMultipleFiles(@RequestParam("files") files: Array<MultipartFile>): List<UploadFileResponse> {
        return files.map { file -> uploadFile(file) }
    }

    @GetMapping("/download/{fileName:.+}")
    fun downloadFile(@PathVariable fileName: String, request: HttpServletRequest): ResponseEntity<Resource> {
        val resource = fileStorageService.loadFileAsResource(fileName)

        var contentType = request.servletContext.getMimeType(resource.file.absolutePath)
        if (contentType == null) {
            contentType = "application/octet-stream"
        }

        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(contentType))
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${resource.filename}\"")
            .body(resource)
    }
}

Response Models

Create data classes for responses:

data class UploadFileResponse(
    val fileName: String,
    val fileDownloadUri: String,
    val fileType: String,
    val size: Long
)

Exception Handler

Add a global exception handler:

@ControllerAdvice
class FileUploadExceptionHandler {
    
    @ExceptionHandler(StorageException::class)
    fun handleStorageException(ex: StorageException): ResponseEntity<ErrorResponse> {
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse("Storage error", ex.message))
    }

    @ExceptionHandler(FileNotFoundException::class)
    fun handleFileNotFoundException(ex: FileNotFoundException): ResponseEntity<ErrorResponse> {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse("File not found", ex.message))
    }

    data class ErrorResponse(
        val error: String,
        val message: String?
    )
}

Usage Examples

Here's how to use the file upload/download endpoints:

Upload a Single File

curl -X POST \
  http://localhost:8080/api/files/upload \
  -H 'content-type: multipart/form-data' \
  -F 'file=@/path/to/your/file.pdf'

Upload Multiple Files

curl -X POST \
  http://localhost:8080/api/files/upload-multiple \
  -H 'content-type: multipart/form-data' \
  -F 'files=@/path/to/file1.pdf' \
  -F 'files=@/path/to/file2.jpg'

Download a File

curl -X GET \
  http://localhost:8080/api/files/download/filename.pdf \
  -O -J

Testing

Here's a basic test for the FileStorageService:

@SpringBootTest
class FileStorageServiceTest {
    
    @Autowired
    private lateinit var fileStorageService: FileStorageService
    
    @Test
    fun `when storing file then file is saved successfully`() {
        // Create a mock MultipartFile
        val content = "test content".toByteArray()
        val file = MockMultipartFile(
            "test.txt",
            "test.txt",
            MediaType.TEXT_PLAIN_VALUE,
            content
        )
        
        // Store the file
        val fileName = fileStorageService.storeFile(file)
        
        // Verify the file exists
        val savedFile = fileStorageService.loadFileAsResource(fileName)
        assertTrue(savedFile.exists())
        assertEquals("test.txt", savedFile.filename)
    }
}

Security Considerations

  1. Always validate file extensions and content types
  2. Implement virus scanning if needed
  3. Use proper file permissions
  4. Consider implementing file size limits
  5. Implement proper authentication and authorization

Error Handling Tips

  1. Handle concurrent file uploads
  2. Implement retry mechanism for failed uploads
  3. Clean up temporary files
  4. Log all file operations
  5. Implement proper exception handling

Latest Articles

Error Handling Best Practices in Spring Boot with Kotlin

Error Handling Best Practices in Spring Boot with Kotlin

4 min read rest · Spring Basics

Master Spring Boot error handling in Kotlin with comprehensive examples: learn to implement global exception handlers, custom error responses, and production-ready error handling strategies

Role-Based Access Control (RBAC) in Spring Security with Kotlin

Role-Based Access Control (RBAC) in Spring Security with Kotlin

4 min read Security · rest

Master Role-Based Access Control (RBAC) in Spring Boot applications using Kotlin with practical examples, from basic setup to advanced configurations with method-level security

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

4 min read Security · rest

Learn how to implement secure API key authentication in Spring Boot with Kotlin, including rate limiting, monitoring, and production-ready best practices - complete with practical examples and ready-to-use code