CODE WITH SIBIN

Solving Real Problems with Real Code


Spring Boot + Vue.js: File Management System

This guide will help you create a complete file management system with Vue 3 frontend and Spring Boot 3 backend.

Project Structure

Frontend (Vue 3) Directory Structure

Backend (Spring Boot 3) Directory Structure

Frontend Implementation

1. Set up Vue 3 project

npm create vue@latest file-manager-frontend
cd file-manager-frontend
npm install axios pinia vue-router
npm run dev

2. Main components

FileUpload.vue

<template>
  <div class="file-upload">
    <input type="file" @change="handleFileChange" multiple />
    <button @click="uploadFiles" :disabled="!files.length">Upload</button>
    <div v-if="progress > 0" class="progress-bar">
      <div :style="{ width: progress + '%' }"></div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useFileStore } from '@/stores/fileStore';

const fileStore = useFileStore();
const files = ref([]);
const progress = ref(0);

const handleFileChange = (e) => {
  files.value = Array.from(e.target.files);
};

const uploadFiles = async () => {
  if (!files.value.length) return;
  
  const formData = new FormData();
  files.value.forEach(file => {
    formData.append('files', file);
  });

  try {
    await fileStore.uploadFiles(formData, (event) => {
      progress.value = Math.round((event.loaded * 100) / event.total);
    });
    files.value = [];
    progress.value = 0;
  } catch (error) {
    console.error('Upload failed:', error);
  }
};
</script>

FileList.vue

<template>
  <div class="file-list">
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Size</th>
          <th>Type</th>
          <th>Uploaded</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="file in files" :key="file.id">
          <td>{{ file.name }}</td>
          <td>{{ formatSize(file.size) }}</td>
          <td>{{ file.type }}</td>
          <td>{{ formatDate(file.uploadedAt) }}</td>
          <td>
            <button @click="downloadFile(file.id, file.name)">Download</button>
            <button @click="deleteFile(file.id)">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
import { computed } from 'vue';
import { useFileStore } from '@/stores/fileStore';

const fileStore = useFileStore();
const files = computed(() => fileStore.files);

const formatSize = (bytes) => {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
};

const formatDate = (dateString) => {
  return new Date(dateString).toLocaleString();
};

const downloadFile = async (id, name) => {
  await fileStore.downloadFile(id, name);
};

const deleteFile = async (id) => {
  if (confirm('Are you sure you want to delete this file?')) {
    await fileStore.deleteFile(id);
  }
};
</script>

fileStore.js (Pinia store)

import { defineStore } from 'pinia';
import { ref } from 'vue';
import axios from 'axios';

export const useFileStore = defineStore('file', () => {
  const files = ref([]);
  const error = ref(null);
  
  const API_URL = 'http://localhost:8080/api/files';

  const fetchFiles = async () => {
    try {
      const response = await axios.get(API_URL);
      files.value = response.data;
    } catch (err) {
      error.value = err.message;
    }
  };

  const uploadFiles = async (formData, onUploadProgress) => {
    try {
      await axios.post(API_URL, formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        },
        onUploadProgress
      });
      await fetchFiles();
    } catch (err) {
      error.value = err.message;
      throw err;
    }
  };

  const downloadFile = async (id, name) => {
    try {
      const response = await axios.get(`${API_URL}/${id}/download`, {
        responseType: 'blob'
      });
      
      const url = window.URL.createObjectURL(new Blob([response.data]));
      const link = document.createElement('a');
      link.href = url;
      link.setAttribute('download', name);
      document.body.appendChild(link);
      link.click();
      link.remove();
    } catch (err) {
      error.value = err.message;
    }
  };

  const deleteFile = async (id) => {
    try {
      await axios.delete(`${API_URL}/${id}`);
      await fetchFiles();
    } catch (err) {
      error.value = err.message;
    }
  };

  return {
    files,
    error,
    fetchFiles,
    uploadFiles,
    downloadFile,
    deleteFile
  };
});

Backend Implementation

1. Set up Spring Boot project

Use Spring Initializr (https://start.spring.io/) with these dependencies:

  • Spring Web
  • Spring Data JPA
  • H2 Database (or your preferred database)
  • Lombok

2. Main components

FileInfo.java (Entity)

package com.example.filemanager.model;

import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;

import java.util.Date;

@Entity
@Data
public class FileInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String type;
    private long size;
    private String storagePath;
    
    @CreationTimestamp
    @Temporal(TemporalType.TIMESTAMP)
    private Date uploadedAt;
}

FileInfoDto.java (DTO)

package com.example.filemanager.dto;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class FileInfoDto {
    private Long id;
    private String name;
    private String type;
    private long size;
    private LocalDateTime uploadedAt;
}

ResponseMessage.java

package com.example.filemanager.dto;

import lombok.Data;

@Data
public class ResponseMessage {
    private String message;
    
    public ResponseMessage(String message) {
        this.message = message;
    }
}

FileStorageService.java (Interface)

package com.example.filemanager.service;

import com.example.filemanager.model.FileInfo;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;

public interface FileStorageService {
    void init();
    FileInfo save(MultipartFile file);
    List<FileInfo> saveAll(MultipartFile[] files);
    Resource load(Long fileId);
    void delete(Long fileId);
    void deleteAll();
    Stream<FileInfo> loadAll();
}

FileStorageServiceImpl.java

package com.example.filemanager.service;

import com.example.filemanager.exception.FileStorageException;
import com.example.filemanager.model.FileInfo;
import com.example.filemanager.repository.FileInfoRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class FileStorageServiceImpl implements FileStorageService {

    @Value("${file.upload-dir}")
    private String uploadDir;
    
    @Autowired
    private FileInfoRepository fileInfoRepository;

    @Override
    public void init() {
        try {
            Files.createDirectories(Paths.get(uploadDir));
        } catch (IOException e) {
            throw new FileStorageException("Could not initialize storage directory", e);
        }
    }

    @Override
    public FileInfo save(MultipartFile file) {
        String filename = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
        try {
            if (filename.contains("..")) {
                throw new FileStorageException("Filename contains invalid path sequence: " + filename);
            }
            
            String uniqueFilename = UUID.randomUUID() + "_" + filename;
            Path targetLocation = Paths.get(uploadDir).resolve(uniqueFilename);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
            
            FileInfo fileInfo = new FileInfo();
            fileInfo.setName(filename);
            fileInfo.setType(file.getContentType());
            fileInfo.setSize(file.getSize());
            fileInfo.setStoragePath(targetLocation.toString());
            
            return fileInfoRepository.save(fileInfo);
        } catch (IOException e) {
            throw new FileStorageException("Failed to store file " + filename, e);
        }
    }

    @Override
    public List<FileInfo> saveAll(MultipartFile[] files) {
        return Arrays.stream(files)
                .map(this::save)
                .collect(Collectors.toList());
    }

    @Override
    public Resource load(Long fileId) {
        FileInfo fileInfo = fileInfoRepository.findById(fileId)
                .orElseThrow(() -> new FileStorageException("File not found with id: " + fileId));
        
        try {
            Path filePath = Paths.get(fileInfo.getStoragePath());
            Resource resource = new UrlResource(filePath.toUri());
            
            if (resource.exists() || resource.isReadable()) {
                return resource;
            } else {
                throw new FileStorageException("Could not read file: " + fileInfo.getName());
            }
        } catch (MalformedURLException e) {
            throw new FileStorageException("Could not read file: " + fileInfo.getName(), e);
        }
    }

    @Override
    public void delete(Long fileId) {
        FileInfo fileInfo = fileInfoRepository.findById(fileId)
                .orElseThrow(() -> new FileStorageException("File not found with id: " + fileId));
        
        try {
            Path filePath = Paths.get(fileInfo.getStoragePath());
            Files.deleteIfExists(filePath);
            fileInfoRepository.delete(fileInfo);
        } catch (IOException e) {
            throw new FileStorageException("Could not delete file: " + fileInfo.getName(), e);
        }
    }

    @Override
    public void deleteAll() {
        fileInfoRepository.deleteAll();
        try {
            Files.walk(Paths.get(uploadDir))
                    .filter(Files::isRegularFile)
                    .forEach(path -> {
                        try {
                            Files.delete(path);
                        } catch (IOException e) {
                            System.err.println("Failed to delete " + path + ": " + e.getMessage());
                        }
                    });
        } catch (IOException e) {
            throw new FileStorageException("Failed to delete files", e);
        }
    }

    @Override
    public Stream<FileInfo> loadAll() {
        return fileInfoRepository.findAll().stream();
    }
}

FileController.java

package com.example.filemanager.controller;

import com.example.filemanager.dto.FileInfoDto;
import com.example.filemanager.dto.ResponseMessage;
import com.example.filemanager.model.FileInfo;
import com.example.filemanager.service.FileStorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
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 java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/files")
@CrossOrigin(origins = "http://localhost:5173", 
             allowedHeaders = "*",
             methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.DELETE},
             allowCredentials = "true")
public class FileController {

    @Autowired
    private FileStorageService storageService;

    @GetMapping
    public ResponseEntity<List<FileInfoDto>> listFiles() {
        List<FileInfoDto> files = storageService.loadAll()
                .map(this::mapToFileInfoDto)
                .collect(Collectors.toList());
        
        return ResponseEntity.ok(files);
    }

    @PostMapping
    public ResponseEntity<ResponseMessage> uploadFiles(@RequestParam("files") MultipartFile[] files) {
        try {
            storageService.saveAll(files);
            return ResponseEntity.ok(new ResponseMessage("Files uploaded successfully"));
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                    .body(new ResponseMessage("Failed to upload files: " + e.getMessage()));
        }
    }

    @GetMapping("/{id}/download")
    public ResponseEntity<Resource> downloadFile(@PathVariable Long id) {
        Resource file = storageService.load(id);
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                        "attachment; filename=\"" + file.getFilename() + "\"")
                .body(file);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<ResponseMessage> deleteFile(@PathVariable Long id) {
        try {
            storageService.delete(id);
            return ResponseEntity.ok(new ResponseMessage("File deleted successfully"));
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                    .body(new ResponseMessage("Failed to delete file: " + e.getMessage()));
        }
    }

    private FileInfoDto mapToFileInfoDto(FileInfo fileInfo) {
        String downloadUrl = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/api/files/")
                .path(fileInfo.getId().toString())
                .path("/download")
                .toUriString();
        
        FileInfoDto dto = new FileInfoDto();
        dto.setId(fileInfo.getId());
        dto.setName(fileInfo.getName());
        dto.setType(fileInfo.getType());
        dto.setSize(fileInfo.getSize());
        dto.setUploadedAt(fileInfo.getUploadedAt().toInstant());
        
        return dto;
    }
}

application.properties

# Server port
server.port=8080

# File storage
file.upload-dir=uploads

# H2 Database
spring.datasource.url=jdbc:h2:mem:filemanager
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true

Running the Application

Backend

  1. Start the Spring Boot application
  2. Access H2 console at http://localhost:8080/h2-console
    • JDBC URL: jdbc:h2:mem:filemanager
    • Username: sa
    • Password: (empty)

Frontend

  1. Run npm run dev
  2. Access the application at http://localhost:5173

Features Implemented

  • File upload (single/multiple)
  • File listing with metadata
  • File download
  • File deletion
  • Progress tracking for uploads
  • Error handling
  • Responsive UI

Security Considerations (To Implement)

  1. Add authentication (JWT)
  2. Implement file access control
  3. Add file size limits
  4. Implement virus scanning for uploads
  5. Add rate limiting

Deployment Considerations

  1. Use proper database (PostgreSQL, MySQL) in production
  2. Configure proper file storage (S3, Azure Blob Storage)
  3. Set up proper CORS configuration
  4. Implement HTTPS

This implementation provides a complete foundation for a file management system that you can extend with additional features as needed.

Leave a Reply

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