Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created November 25, 2025 14:16
Show Gist options
  • Select an option

  • Save sunmeat/452abc75a6760dc9a253833aaa9b8e20 to your computer and use it in GitHub Desktop.

Select an option

Save sunmeat/452abc75a6760dc9a253833aaa9b8e20 to your computer and use it in GitHub Desktop.
завантаження картинок через FTP + spring boot
build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.0-SNAPSHOT'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'site.sunmeat'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
maven { url = 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'commons-net:commons-net:3.11.0' // !!!
}
tasks.named('test') {
useJUnitPlatform()
}
=================================================================================================================
FtpService.java:
package site.sunmeat.hibernate;
import org.apache.commons.net.ftp.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.*;
@Service
public class FtpService {
@Value("${ftp.host}") private String host;
@Value("${ftp.port:21}") private int port;
@Value("${ftp.username}") private String username;
@Value("${ftp.password}") private String password;
@Value("${ftp.remoteDir}") private String remoteDir;
@Value("${ftp.baseUrl}") private String baseUrl;
public String uploadImage(MultipartFile file) throws Exception {
var ftp = new FTPClient();
try {
ftp.connect(host, port);
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
throw new Exception("FTP сервер відхилив підключення: " + reply);
}
if (!ftp.login(username, password)) {
throw new Exception("Не вдалося увійти (неправильний логін/пароль)");
}
ftp.enterLocalPassiveMode();
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);
if (!ftp.changeWorkingDirectory(remoteDir)) {
System.out.println("Директорія не існує, спроба створити: " + remoteDir);
ftp.makeDirectory(remoteDir);
ftp.changeWorkingDirectory(remoteDir);
}
System.out.println("Поточна директорія: " + ftp.printWorkingDirectory());
String ext = file.getOriginalFilename() != null &&
file.getOriginalFilename().contains(".")
? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
: ".jpg";
String fileName = UUID.randomUUID() + ext.toLowerCase();
try (InputStream in = file.getInputStream()) {
boolean success = ftp.storeFile(fileName, in);
if (!success) {
throw new Exception("Не вдалося зберегти файл: " + ftp.getReplyString());
}
}
return fileName;
} finally {
if (ftp.isConnected())
try { ftp.logout(); ftp.disconnect(); } catch (Exception ignored) {}
}
}
public byte[] downloadImage(String fileName) throws Exception {
var ftp = new FTPClient();
try {
ftp.connect(host, port);
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply))
throw new Exception("FTP сервер відхилив підключення: " + reply);
if (!ftp.login(username, password))
throw new Exception("Не вдалося увійти");
ftp.enterLocalPassiveMode();
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);
ftp.changeWorkingDirectory(remoteDir);
var outputStream = new ByteArrayOutputStream();
boolean success = ftp.retrieveFile(fileName, outputStream);
if (!success)
throw new Exception("Не вдалося завантажити файл: " + fileName);
return outputStream.toByteArray();
} finally {
if (ftp.isConnected())
try { ftp.logout(); ftp.disconnect(); } catch (Exception ignored) {}
}
}
public String[] listImages() throws Exception {
var ftp = new FTPClient();
try {
System.out.println("Підключення до FTP...");
ftp.connect(host, port);
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply))
throw new Exception("FTP сервер відхилив підключення: " + reply);
System.out.println("Логін...");
if (!ftp.login(username, password))
throw new Exception("Не вдалося увійти");
ftp.enterLocalPassiveMode();
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);
System.out.println("Поточна директорія до зміни: " + ftp.printWorkingDirectory());
boolean changed = ftp.changeWorkingDirectory(remoteDir);
System.out.println("Зміна директорії на '" + remoteDir + "': " + (changed ? "OK" : "FAILED"));
System.out.println("Поточна директорія після зміни: " + ftp.printWorkingDirectory());
FTPFile[] listings = ftp.listFiles();
System.out.println("Знайдено файлів: " + (listings != null ? listings.length : 0));
if (listings == null) {
System.out.println("listFiles() повернув null!");
return new String[0];
}
List<String> realFiles = new ArrayList<>();
for (FTPFile f : listings) {
if (f == null) continue;
System.out.println("Файл: " + f.getName() + " | Директорія: " + f.isDirectory() + " | Розмір: " + f.getSize());
if (f.isDirectory()) continue;
if (".".equals(f.getName()) || "..".equals(f.getName())) continue;
String name = f.getName();
if (name.startsWith("./")) name = name.substring(2);
if (name.startsWith("/")) name = name.substring(1);
realFiles.add(name);
}
System.out.println("Реальних файлів після фільтрації: " + realFiles.size());
return realFiles.toArray(new String[0]);
} catch (Exception e) {
System.err.println("Помилка в listImages(): " + e.getMessage());
throw e;
} finally {
if (ftp.isConnected()) {
try { ftp.logout(); ftp.disconnect(); } catch (Exception ignored) {}
}
}
}
}
=================================================================================================================
WebController.java:
package site.sunmeat.hibernate;
import org.springframework.http.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
@Controller
public class WebController {
private final FtpService ftpService;
public WebController(FtpService ftpService) {
this.ftpService = ftpService;
}
@GetMapping("/")
public String index() {
return "index";
}
@PostMapping("/api/upload-photo")
@ResponseBody
public Map<String, String> uploadPhoto(@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty())
return Map.of("title", "Помилка", "content", "Виберіть фото!");
String fileName = ftpService.uploadImage(file);
String url = "/api/image/" + fileName;
return Map.of(
"title", "Готово!",
"content", "<strong>Фото успішно завантажено!</strong><br><img src='" + url + "' style='max-width:100%; border-radius:12px; margin-top:15px; box-shadow:0 4px 20px rgba(0,0,0,0.2);'>"
);
} catch (Exception e) {
return Map.of("title", "Помилка", "content", "Не вдалося зберегти: " + e.getMessage());
}
}
@GetMapping("/api/image/{fileName}")
public ResponseEntity<byte[]> getImage(@PathVariable("fileName") String fileName) {
try {
byte[] imageData = ftpService.downloadImage(fileName);
MediaType mediaType = MediaType.IMAGE_JPEG;
String lowerName = fileName.toLowerCase();
if (lowerName.endsWith(".png")) {
mediaType = MediaType.IMAGE_PNG;
} else if (lowerName.endsWith(".gif")) {
mediaType = MediaType.IMAGE_GIF;
} else if (lowerName.endsWith(".webp")) {
mediaType = MediaType.parseMediaType("image/webp");
}
var headers = new HttpHeaders();
headers.setContentType(mediaType);
headers.setContentLength(imageData.length);
return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
} catch (Exception e) {
System.err.println("Помилка завантаження зображення " + fileName + ": " + e.getMessage());
return ResponseEntity.notFound().build();
}
}
@GetMapping("/api/gallery")
@ResponseBody
public Map<String, Object> getGallery() {
try {
String[] files = ftpService.listImages();
List<String> photos = new ArrayList<>();
for (String fileName : files) {
if (fileName == null || fileName.trim().isEmpty()) continue;
String lowerName = fileName.toLowerCase().trim();
if (lowerName.endsWith(".jpg") ||
lowerName.endsWith(".jpeg") ||
lowerName.endsWith(".png") ||
lowerName.endsWith(".gif") ||
lowerName.endsWith(".webp")) {
// локальний проксі-ендпоїнт, awardspace не дає забрати картинки напряму!
String url = "/api/image/" + fileName;
System.out.println("Додаємо фото: " + fileName + " -> " + url);
photos.add(url);
}
}
System.out.println("Всього фото в галереї: " + photos.size());
return Map.of(
"title", "Галерея (" + photos.size() + " фото)",
"photos", photos
);
} catch (Exception e) {
e.printStackTrace();
return Map.of("title", "Помилка", "photos", List.of(), "error", e.getMessage());
}
}
}
=================================================================================================================
index.html:
<!DOCTYPE html>
<html lang="uk" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Галерея</title>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Playfair+Display:wght@700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" th:href="@{style.css}">
</head>
<body>
<div class="container">
<h1>Галерея фото</h1>
<div class="upload-form">
<input type="file" id="photoInput" accept="image/*">
<button onclick="uploadPhoto()">Завантажити фото</button>
</div>
<div class="buttons">
<button onclick="loadGallery()">Показати галерею</button>
</div>
<div id="result" class="result-card">
<div class="title" id="resultTitle">Завантажте фото або покажіть галерею</div>
<div id="resultContent"></div>
</div>
</div>
<script>
document.querySelectorAll('button').forEach(btn => {
btn.addEventListener('mousemove', e => {
const rect = btn.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100 + '%';
const y = ((e.clientY - rect.top) / rect.height) * 100 + '%';
btn.style.setProperty('--x', x);
btn.style.setProperty('--y', y);
});
});
async function uploadPhoto() {
const fileInput = document.getElementById('photoInput');
const file = fileInput.files[0];
if (!file) {
showResult("Помилка", "Виберіть фото!");
return;
}
const formData = new FormData();
formData.append('file', file);
showResult("Завантаження...", "Зачекайте...");
try {
const res = await fetch('/api/upload-photo', {
method: 'POST',
body: formData
});
const data = await res.json();
showResult(data.title, data.content);
fileInput.value = '';
} catch (err) {
console.error('Помилка завантаження:', err);
showResult("Помилка", "Не вдалося завантажити: " + err.message);
}
}
async function loadGallery() {
showResult("Завантаження галереї...", "Зачекайте...");
try {
const res = await fetch('/api/gallery');
const data = await res.json();
console.log('Отримані дані галереї:', data);
if (data.photos.length === 0) {
showResult(data.title, "<pre>Галерея порожня</pre>");
return;
}
let html = `<div class="gallery-grid">`;
data.photos.forEach((src, index) => {
console.log(`Фото ${index + 1}: ${src}`);
html += `<div class="photo-card">
<img src="${src}"
alt="Фото ${index + 1}"
onerror="this.style.border='3px solid red'; console.error('НЕ ЗАГРУЗИЛОСЬ:', '${src}');"
onload="console.log('✓ Загружено:', '${src}');">
</div>`;
});
html += `</div>`;
showResult(data.title, html);
} catch (err) {
console.error('Помилка завантаження галереї:', err);
showResult("Помилка", "Не вдалося завантажити галерею: " + err.message);
}
}
function showResult(title, content) {
document.getElementById('resultTitle').textContent = title;
document.getElementById('resultContent').innerHTML = content;
document.getElementById('result').style.display = 'block';
}
</script>
</body>
</html>
=================================================================================================================
application.properties:
spring.application.name=hibernate
ftp.host=f33-preview.awardspace.net
ftp.port=21
ftp.username=4115733_java
ftp.password=YOUR_PASSWORD_HERE!!!
ftp.remoteDir=/images
ftp.baseUrl=https://sunmeat.atwebpages.com/images/
logging.level.site.sunmeat.hibernate=DEBUG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment