CODE WITH SIBIN

Solving Real Problems with Real Code


Svelte + Spring Boot File Upload, List, Download, Delete Complete Example

Here's a comprehensive example of a file management system with Svelte frontend and Spring Boot backend, including file upload, listing, downloading, and deletion functionality.

Project Directory Structure

Backend Implementation (Spring Boot)

1. FileController.java

package com.example.filemanagement.controller;

import com.example.filemanagement.exception.MyFileNotFoundException;
import com.example.filemanagement.model.FileInfo;
import com.example.filemanagement.payload.ResponseMessage;
import com.example.filemanagement.payload.UploadFileResponse;
import com.example.filemanagement.service.FileStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/files")
public class FileController {

    @Autowired
    private FileStorageService fileStorageService;

    @PostMapping("/upload")
    public ResponseEntity<UploadFileResponse> uploadFile(@RequestParam("file") MultipartFile file) {
        String fileName = fileStorageService.storeFile(file);

        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/api/files/download/")
                .path(fileName)
                .toUriString();

        return ResponseEntity.ok(new UploadFileResponse(
                fileName, 
                fileDownloadUri, 
                file.getContentType(), 
                file.getSize()));
    }

    @PostMapping("/upload-multiple")
    public ResponseEntity<List<UploadFileResponse>> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        return ResponseEntity.ok(Arrays.asList(files)
                .stream()
                .map(file -> uploadFile(file).getBody())
                .collect(Collectors.toList());
    }

    @GetMapping("/download/{fileName:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
        Resource resource = fileStorageService.loadFileAsResource(fileName);
        
        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            // Fallback to default content type
            contentType = "application/octet-stream";
        }
        
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                        "attachment; filename=\"" + resource.getFilename() + "\"")
                .body(resource);
    }

    @GetMapping("/list")
    public ResponseEntity<List<FileInfo>> listFiles() {
        List<FileInfo> fileInfos = fileStorageService.loadAllFiles()
                .map(path -> {
                    String filename = path.getFileName().toString();
                    String url = ServletUriComponentsBuilder.fromCurrentContextPath()
                            .path("/api/files/download/")
                            .path(filename)
                            .toUriString();
                    
                    return new FileInfo(filename, url);
                })
                .collect(Collectors.toList());
        
        return ResponseEntity.ok(fileInfos);
    }

    @DeleteMapping("/delete/{fileName:.+}")
    public ResponseEntity<ResponseMessage> deleteFile(@PathVariable String fileName) {
        fileStorageService.deleteFile(fileName);
        return ResponseEntity.ok(new ResponseMessage("File deleted successfully: " + fileName));
    }
}

2. FileStorageService.java

package com.example.filemanagement.service;

import com.example.filemanagement.exception.FileStorageException;
import com.example.filemanagement.exception.MyFileNotFoundException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;

@Service
public class FileStorageService {

    private final Path fileStorageLocation;

    public FileStorageService(@Value("${file.upload-dir}") String uploadDir) {
        this.fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
        
        try {
            Files.createDirectories(this.fileStorageLocation);
        } catch (Exception ex) {
            throw new FileStorageException("Could not create upload directory", ex);
        }
    }

    public String storeFile(MultipartFile file) {
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());
        
        try {
            if (fileName.contains("..")) {
                throw new FileStorageException("Invalid file path: " + fileName);
            }
            
            Path targetLocation = this.fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
            
            return fileName;
        } catch (IOException ex) {
            throw new FileStorageException("Could not store file " + fileName, ex);
        }
    }

    public Resource loadFileAsResource(String fileName) {
        try {
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            
            if (resource.exists()) {
                return resource;
            } else {
                throw new MyFileNotFoundException("File not found: " + fileName);
            }
        } catch (MalformedURLException ex) {
            throw new MyFileNotFoundException("File not found: " + fileName, ex);
        }
    }

    public Stream<Path> loadAllFiles() {
        try {
            return Files.walk(this.fileStorageLocation, 1)
                    .filter(path -> !path.equals(this.fileStorageLocation))
                    .map(this.fileStorageLocation::relativize);
        } catch (IOException ex) {
            throw new FileStorageException("Failed to read stored files", ex);
        }
    }

    public void deleteFile(String fileName) {
        try {
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            Files.deleteIfExists(filePath);
        } catch (IOException ex) {
            throw new FileStorageException("Could not delete file: " + fileName, ex);
        }
    }
}

3. Other Backend Classes

// FileInfo.java (Model)
package com.example.filemanagement.model;

public class FileInfo {
    private String name;
    private String url;

    public FileInfo(String name, String url) {
        this.name = name;
        this.url = url;
    }

    // Getters and setters
}

// ResponseMessage.java (Payload)
package com.example.filemanagement.payload;

public class ResponseMessage {
    private String message;

    public ResponseMessage(String message) {
        this.message = message;
    }

    // Getter and setter
}

// UploadFileResponse.java (Payload)
package com.example.filemanagement.payload;

public class UploadFileResponse {
    private String fileName;
    private String fileDownloadUri;
    private String fileType;
    private long size;

    public UploadFileResponse(String fileName, String fileDownloadUri, String fileType, long size) {
        this.fileName = fileName;
        this.fileDownloadUri = fileDownloadUri;
        this.fileType = fileType;
        this.size = size;
    }

    // Getters and setters
}

// WebConfig.java (CORS Configuration)
package com.example.filemanagement.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:5000") // Svelte dev server
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

4. application.properties

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

Frontend Implementation (Svelte)

1. FileUpload.svelte

<script>
  import { createEventDispatcher } from 'svelte';
  import { files } from '../stores/files';
  import api from '../utils/api';
  
  const dispatch = createEventDispatcher();
  
  let selectedFile = null;
  let isUploading = false;
  let uploadProgress = 0;
  let error = null;
  
  const handleFileChange = (e) => {
    selectedFile = e.target.files[0];
  };
  
  const uploadFile = async () => {
    if (!selectedFile) return;
    
    isUploading = true;
    error = null;
    
    try {
      const formData = new FormData();
      formData.append('file', selectedFile);
      
      const response = await api.uploadFile(formData, (progressEvent) => {
        uploadProgress = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        );
      });
      
      dispatch('uploaded', response);
      selectedFile = null;
    } catch (err) {
      error = err.message || 'Upload failed';
    } finally {
      isUploading = false;
      uploadProgress = 0;
    }
  };
</script>

<div class="file-upload">
  <h2>Upload File</h2>
  
  {#if error}
    <div class="error">{error}</div>
  {/if}
  
  <div class="file-input">
    <input 
      type="file" 
      id="file" 
      on:change={handleFileChange} 
      disabled={isUploading}
    />
    <button 
      on:click={uploadFile} 
      disabled={!selectedFile || isUploading}
    >
      {isUploading ? 'Uploading...' : 'Upload'}
    </button>
  </div>
  
  {#if isUploading}
    <progress value={uploadProgress} max="100">{uploadProgress}%</progress>
  {/if}
  
  {#if selectedFile}
    <div class="file-info">
      <p>Selected file: {selectedFile.name}</p>
      <p>Size: {(selectedFile.size / 1024).toFixed(2)} KB</p>
    </div>
  {/if}
</div>

<style>
  .file-upload {
    margin: 2rem 0;
    padding: 1.5rem;
    border: 1px solid #ddd;
    border-radius: 8px;
    background: #f9f9f9;
  }
  
  .error {
    color: #ff3e00;
    margin-bottom: 1rem;
  }
  
  .file-input {
    display: flex;
    gap: 1rem;
    margin-bottom: 1rem;
  }
  
  button {
    padding: 0.5rem 1rem;
    background: #ff3e00;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  
  button:disabled {
    background: #ccc;
    cursor: not-allowed;
  }
  
  progress {
    width: 100%;
    margin: 1rem 0;
  }
  
  .file-info {
    margin-top: 1rem;
    font-size: 0.9rem;
    color: #666;
  }
</style>

2. FileList.svelte

<script>
  import { onMount } from 'svelte';
  import { files, fetchFiles } from '../stores/files';
  import api from '../utils/api';
  
  let isLoading = false;
  let error = null;
  
  onMount(() => {
    fetchFiles();
  });
  
  const handleDownload = async (fileName, fileUrl) => {
    try {
      await api.downloadFile(fileUrl, fileName);
    } catch (err) {
      error = err.message || 'Download failed';
    }
  };
  
  const handleDelete = async (fileName) => {
    try {
      await api.deleteFile(fileName);
      await fetchFiles();
    } catch (err) {
      error = err.message || 'Delete failed';
    }
  };
</script>

<div class="file-list">
  <h2>Files</h2>
  
  {#if error}
    <div class="error">{error}</div>
  {/if}
  
  {#if isLoading}
    <div class="loading">Loading files...</div>
  {:else}
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        {#each $files as file}
          <tr>
            <td>{file.name}</td>
            <td class="actions">
              <button on:click={() => handleDownload(file.name, file.url)}>
                Download
              </button>
              <button 
                class="delete" 
                on:click={() => handleDelete(file.name)}
              >
                Delete
              </button>
            </td>
          </tr>
        {/each}
      </tbody>
    </table>
    
    {#if $files.length === 0}
      <div class="empty">No files uploaded yet.</div>
    {/if}
  {/if}
</div>

<style>
  .file-list {
    margin: 2rem 0;
    padding: 1.5rem;
    border: 1px solid #ddd;
    border-radius: 8px;
    background: #f9f9f9;
  }
  
  .error {
    color: #ff3e00;
    margin-bottom: 1rem;
  }
  
  .loading, .empty {
    text-align: center;
    padding: 1rem;
    color: #666;
  }
  
  table {
    width: 100%;
    border-collapse: collapse;
  }
  
  th, td {
    padding: 0.75rem;
    text-align: left;
    border-bottom: 1px solid #ddd;
  }
  
  th {
    background: #eee;
  }
  
  .actions {
    display: flex;
    gap: 0.5rem;
  }
  
  button {
    padding: 0.25rem 0.5rem;
    background: #ff3e00;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  
  button.delete {
    background: #ff0000;
  }
</style>

3. files.js (Store)

import { writable } from 'svelte/store';
import api from '../utils/api';

export const files = writable([]);

export const fetchFiles = async () => {
  try {
    const fileList = await api.getFiles();
    files.set(fileList);
  } catch (error) {
    console.error('Failed to fetch files:', error);
    files.set([]);
  }
};

4. api.js (API Utility)

const API_BASE = 'http://localhost:8080/api/files';

export default {
  async getFiles() {
    const response = await fetch(`${API_BASE}/list`);
    if (!response.ok) throw new Error('Failed to fetch files');
    return await response.json();
  },
  
  async uploadFile(file, onProgress) {
    const formData = new FormData();
    formData.append('file', file);
    
    const xhr = new XMLHttpRequest();
    xhr.open('POST', `${API_BASE}/upload`);
    
    return new Promise((resolve, reject) => {
      xhr.upload.onprogress = (event) => {
        if (event.lengthComputable && onProgress) {
          onProgress(event);
        }
      };
      
      xhr.onload = () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.response));
        } else {
          reject(new Error(xhr.statusText));
        }
      };
      
      xhr.onerror = () => reject(new Error('Network error'));
      xhr.send(formData);
    });
  },
  
  async downloadFile(url, fileName) {
    const response = await fetch(url);
    if (!response.ok) throw new Error('Failed to download file');
    
    const blob = await response.blob();
    const downloadUrl = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(downloadUrl);
    a.remove();
  },
  
  async deleteFile(fileName) {
    const response = await fetch(`${API_BASE}/delete/${fileName}`, {
      method: 'DELETE'
    });
    if (!response.ok) throw new Error('Failed to delete file');
    return await response.json();
  }
};

5. App.svelte

<script>
  import FileUpload from './lib/components/FileUpload.svelte';
  import FileList from './lib/components/FileList.svelte';
  import { fetchFiles } from './lib/stores/files';
  
  const handleUploaded = () => {
    fetchFiles();
  };
</script>

<main>
  <h1>File Management System</h1>
  
  <FileUpload on:uploaded={handleUploaded} />
  <FileList />
</main>

<style>
  main {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  h1 {
    color: #ff3e00;
    text-align: center;
    margin-bottom: 2rem;
  }
</style>

Running the Application

  1. Backend Setup:
    • Create the upload directory specified in application.properties
    • Run the Spring Boot application (mvn spring-boot:run or through your IDE)
  2. Frontend Setup:
    • Install dependencies: npm install
    • Run the Svelte app: npm run dev

Key Features Implemented

  1. File Upload:
    • Single file upload with progress tracking
    • Multi-file upload capability in backend
    • File size validation (10MB limit)
  2. File Listing:
    • Display all uploaded files
    • Shows file names and download URLs
  3. File Download:
    • Downloads files with original names
    • Handles different content types properly
  4. File Deletion:
    • Removes files from server storage
    • Updates UI immediately after deletion
  5. Error Handling:
    • Frontend and backend error handling
    • User-friendly error messages
  6. UI/UX:
    • Progress indicators for uploads
    • Disabled buttons during operations
    • Clean, responsive design

This complete example provides a solid foundation for a file management system that you can extend with additional features like file previews, user authentication, or more advanced file organization.

Leave a Reply

Your email address will not be published. Required fields are marked *