rest 3 min read

File Upload and Download with Spring Boot and Kotlin

Master file operations in Spring Boot with Kotlin: learn to implement secure file uploads and downloads, handle multiple files, track progress, and follow security best practices. Complete with production-ready code examples.

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