Setting Up Nginx for Image Storage on Ubuntu
Installing Nginx
sudo apt update && sudo apt install nginx
Creating the Storage Directory
sudo mkdir -p /var/www/images/blog
sudo chmod -R 777 /var/www/images/blog
Nginx Configuration
Edit the configuration file at /etc/nginx/sites-available/default:
server {
listen 80;
server_name your-domain.com;
location /images/ {
root /var/www;
autoindex on;
expires 30d;
}
}
Apply the changes:
sudo systemctl restart nginx
Security Enhancements
HTTPS Implementation: Obtain a free SSL certificate through Let's Encrypt.
File Type Restrictions:
if ($request_filename ~* \.(php|jsp)) { deny all; }
Hotlink Protection:
valid_referers blocked server_names;
if ($invalid_referer) { return 403; }
Spring Boot Backend Implementation
Upload Controller
@RestController
@RequestMapping("/api/upload")
public class ImageController {
@Value("${file.storage-path}")
private String storagePath;
@PostMapping("/image")
public ResponseEntity<Map<String, String>> handleImageUpload(
@RequestParam("file") MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
String generatedName = UUID.randomUUID() + "." + extension;
Path destination = Paths.get(storagePath).resolve(generatedName);
try {
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to save image");
}
String imageUrl = "https://your-domain.com/images/blog/" + generatedName;
return ResponseEntity.ok(Collections.singletonMap("url", imageUrl));
}
}
Application Configuration
In application.yml:
file:
storage-path: /var/www/images/blog
CORS Configuration
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
}
Vue Front end Implementation
Image Upload Component
<template>
<el-upload
action="#"
:http-request="handleUpload"
:show-file-list="false"
:before-upload="validateFile">
<el-button type="primary">Upload Image</el-button>
</el-upload>
</template>
<script setup>
import axios from 'axios';
const handleUpload = async ({ file }) => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await axios.post('/api/upload/image', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
// Insert the returned URL into the rich text editor
richEditor.insertEmbed(
richEditor.getSelection().index,
'image',
response.data.url
);
} catch (error) {
console.error('Upload failed', error);
}
};
const validateFile = (file) => {
const allowedTypes = ['image/jpeg', 'image/png'];
const maxSize = 5 * 1024 * 1024;
if (!allowedTypes.includes(file.type)) {
ElMessage.error('Only JPG/PNG formats are supported');
return false;
}
if (file.size > maxSize) {
ElMessage.error('File size cannot exceed 5MB');
return false;
}
return true;
};
</script>
Rich Text Editor Integration
const richEditor = new Quill('#editor', {
modules: {
toolbar: [
['bold', 'italic'],
['image']
]
}
});
richEditor.getModule('toolbar').addHandler('image', () => {
document.querySelector('.el-upload input').click();
});
Database Schema
Blog Table Structure
CREATE TABLE blog_posts (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content LONGTEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Entity Class
@Entity
@Table(name = "blog_posts")
public class BlogPost {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Lob
private String content;
@CreationTimestamp
private LocalDateTime createdAt;
}