Created
November 25, 2025 14:16
-
-
Save sunmeat/452abc75a6760dc9a253833aaa9b8e20 to your computer and use it in GitHub Desktop.
завантаження картинок через FTP + spring boot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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