finish plugin function
This commit is contained in:
@@ -17,13 +17,13 @@ public class MvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private void exposeDirectory(String dirName, ResourceHandlerRegistry registry) {
|
||||
Path uploadDir = Paths.get(dirName);
|
||||
String uploadPath = uploadDir.toFile().getAbsolutePath();
|
||||
|
||||
if (dirName.startsWith("../")) {
|
||||
dirName = dirName.replace("../", "");
|
||||
}
|
||||
|
||||
String uploadPath = uploadDir.toFile().getAbsolutePath();
|
||||
registry.addResourceHandler("/" + dirName + "/**")
|
||||
.addResourceLocations("file:/" + uploadPath + "/");
|
||||
.addResourceLocations("file:" + uploadPath + "/");
|
||||
}
|
||||
}
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Media;
|
||||
import com.sisvietnamvn.web.domain.MediaType;
|
||||
import com.sisvietnamvn.web.hook.HookManager;
|
||||
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||
import com.sisvietnamvn.web.service.MediaService;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
/**
|
||||
* Thymeleaf MVC controller for the Media Library admin pages.
|
||||
* Provides Library view, Add New upload, Detail/Edit, and Delete.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/manage/media")
|
||||
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")")
|
||||
public class ManageMediaController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManageMediaController.class);
|
||||
|
||||
private final MediaService mediaService;
|
||||
private final HookManager hookManager;
|
||||
|
||||
public ManageMediaController(MediaService mediaService, HookManager hookManager) {
|
||||
this.mediaService = mediaService;
|
||||
this.hookManager = hookManager;
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public void adminInit() {
|
||||
hookManager.doAction("admin_init");
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/media — Media Library page.
|
||||
*/
|
||||
@GetMapping
|
||||
public String listMedia(@RequestParam(value = "type", required = false) String typeStr,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
Model model) {
|
||||
LOG.debug("Request to list all media (type={}, keyword={})", typeStr, keyword);
|
||||
|
||||
MediaType type = null;
|
||||
if (typeStr != null && !typeStr.isBlank()) {
|
||||
try {
|
||||
type = MediaType.valueOf(typeStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid media type filter: {}", typeStr);
|
||||
}
|
||||
}
|
||||
|
||||
model.addAttribute("mediaList", mediaService.findFiltered(type, keyword));
|
||||
model.addAttribute("mediaTypes", MediaType.values());
|
||||
model.addAttribute("selectedType", typeStr);
|
||||
model.addAttribute("keyword", keyword);
|
||||
return "manage/media/list";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/media/new — Add New upload page.
|
||||
*/
|
||||
@GetMapping("/new")
|
||||
public String showUploadPage() {
|
||||
LOG.debug("Request to show media upload page");
|
||||
return "manage/media/upload";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/media/upload — Handle file upload from the form.
|
||||
*/
|
||||
@PostMapping("/upload")
|
||||
public String uploadMedia(@RequestParam("files") MultipartFile[] files,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to upload {} file(s)", files.length);
|
||||
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
if (file.isEmpty()) continue;
|
||||
try {
|
||||
mediaService.upload(file);
|
||||
successCount++;
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to upload file: {}", file.getOriginalFilename(), e);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
redirectAttributes.addFlashAttribute("successMessage",
|
||||
successCount + " file(s) uploaded successfully!");
|
||||
}
|
||||
if (failCount > 0) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage",
|
||||
failCount + " file(s) failed to upload.");
|
||||
}
|
||||
|
||||
return "redirect:/manage/media";
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /manage/media/{id} — View/edit detail page for a single media item.
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public String showDetail(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to show media detail: {}", id);
|
||||
Optional<Media> mediaOpt = mediaService.findById(id);
|
||||
if (mediaOpt.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Media not found.");
|
||||
return "redirect:/manage/media";
|
||||
}
|
||||
Media media = mediaOpt.get();
|
||||
String fullUrl = org.springframework.web.servlet.support.ServletUriComponentsBuilder
|
||||
.fromCurrentContextPath()
|
||||
.path(media.getFileUrl())
|
||||
.toUriString();
|
||||
|
||||
model.addAttribute("media", media);
|
||||
model.addAttribute("fullUrl", fullUrl);
|
||||
return "manage/media/detail";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/media/{id} — Save edits (e.g. alt text) for a media item.
|
||||
*/
|
||||
@PostMapping("/{id}")
|
||||
public String updateMedia(@PathVariable Long id,
|
||||
@RequestParam(value = "altText", required = false) String altText,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to update media: {}", id);
|
||||
Optional<Media> mediaOpt = mediaService.findById(id);
|
||||
if (mediaOpt.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Media not found.");
|
||||
return "redirect:/manage/media";
|
||||
}
|
||||
Media media = mediaOpt.get();
|
||||
media.setAltText(altText);
|
||||
mediaService.save(media);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Media updated successfully!");
|
||||
return "redirect:/manage/media/" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/media/{id}/delete — Delete a media item.
|
||||
*/
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deleteMedia(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to delete media: {}", id);
|
||||
mediaService.delete(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Media deleted successfully!");
|
||||
return "redirect:/manage/media";
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Plugin;
|
||||
import com.sisvietnamvn.web.service.PluginService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/manage/plugins")
|
||||
public class ManagePluginController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ManagePluginController.class);
|
||||
|
||||
private final PluginService pluginService;
|
||||
|
||||
public ManagePluginController(PluginService pluginService) {
|
||||
this.pluginService = pluginService;
|
||||
}
|
||||
|
||||
// --- Installed Plugins List ---
|
||||
|
||||
@GetMapping
|
||||
public String listPlugins(Model model) {
|
||||
LOG.debug("Request to list all plugins");
|
||||
model.addAttribute("plugins", pluginService.findAll());
|
||||
return "manage/plugins/list";
|
||||
}
|
||||
|
||||
// --- Activate / Deactivate / Delete ---
|
||||
|
||||
@PostMapping("/{id}/activate")
|
||||
public String activatePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
Plugin plugin = pluginService.activate(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Plugin '" + plugin.getName() + "' activated successfully. Please restart the application to apply hooks.");
|
||||
} catch (Exception e) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Failed to activate plugin: " + e.getMessage());
|
||||
}
|
||||
return "redirect:/manage/plugins";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/deactivate")
|
||||
public String deactivatePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
Plugin plugin = pluginService.deactivate(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Plugin '" + plugin.getName() + "' deactivated successfully. Please restart the application to remove hooks.");
|
||||
} catch (Exception e) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Failed to deactivate plugin: " + e.getMessage());
|
||||
}
|
||||
return "redirect:/manage/plugins";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String deletePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
pluginService.delete(id);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Plugin deleted successfully.");
|
||||
} catch (Exception e) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Failed to delete plugin: " + e.getMessage());
|
||||
}
|
||||
return "redirect:/manage/plugins";
|
||||
}
|
||||
|
||||
// --- Add New Plugin ---
|
||||
|
||||
@GetMapping("/new")
|
||||
public String showAddPluginPage() {
|
||||
return "manage/plugins/add-new";
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public String uploadPlugin(@RequestParam("zipFile") MultipartFile zipFile, RedirectAttributes redirectAttributes) {
|
||||
if (zipFile.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Please select a .zip file to upload.");
|
||||
return "redirect:/manage/plugins/new";
|
||||
}
|
||||
|
||||
try {
|
||||
pluginService.installFromZip(zipFile);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Plugin installed successfully. You can now activate it.");
|
||||
return "redirect:/manage/plugins";
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to install plugin", e);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Failed to install plugin: " + e.getMessage());
|
||||
return "redirect:/manage/plugins/new";
|
||||
} catch (IllegalArgumentException e) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", e.getMessage());
|
||||
return "redirect:/manage/plugins/new";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Plugin File Editor ---
|
||||
|
||||
@GetMapping("/editor")
|
||||
public String showEditorSelector(Model model) {
|
||||
model.addAttribute("plugins", pluginService.findAll());
|
||||
return "manage/plugins/editor";
|
||||
}
|
||||
|
||||
@GetMapping("/editor/{pluginKey}")
|
||||
public String showEditor(@PathVariable String pluginKey,
|
||||
@RequestParam(value = "file", required = false) String file,
|
||||
Model model, RedirectAttributes redirectAttributes) {
|
||||
Optional<Plugin> pluginOpt = pluginService.findByKey(pluginKey);
|
||||
if (pluginOpt.isEmpty()) {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Plugin not found.");
|
||||
return "redirect:/manage/plugins/editor";
|
||||
}
|
||||
|
||||
model.addAttribute("plugins", pluginService.findAll());
|
||||
model.addAttribute("selectedPlugin", pluginOpt.get());
|
||||
|
||||
try {
|
||||
List<String> files = pluginService.getPluginFiles(pluginKey);
|
||||
model.addAttribute("pluginFiles", files);
|
||||
|
||||
if (file != null && !file.isEmpty() && files.contains(file)) {
|
||||
String content = pluginService.readPluginFile(pluginKey, file);
|
||||
model.addAttribute("selectedFile", file);
|
||||
model.addAttribute("fileContent", content);
|
||||
} else if (!files.isEmpty()) {
|
||||
// Select first file by default
|
||||
String firstFile = files.get(0);
|
||||
String content = pluginService.readPluginFile(pluginKey, firstFile);
|
||||
model.addAttribute("selectedFile", firstFile);
|
||||
model.addAttribute("fileContent", content);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error reading plugin files for {}", pluginKey, e);
|
||||
model.addAttribute("errorMessage", "Error reading plugin files: " + e.getMessage());
|
||||
}
|
||||
|
||||
return "manage/plugins/editor";
|
||||
}
|
||||
|
||||
@PostMapping("/editor/{pluginKey}/save")
|
||||
public String savePluginFile(@PathVariable String pluginKey,
|
||||
@RequestParam("file") String file,
|
||||
@RequestParam("content") String content,
|
||||
RedirectAttributes redirectAttributes) {
|
||||
try {
|
||||
pluginService.writePluginFile(pluginKey, file, content);
|
||||
redirectAttributes.addFlashAttribute("successMessage", "File '" + file + "' saved successfully. Changes may require a rebuild or restart.");
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error saving plugin file {}/{}", pluginKey, file, e);
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "Failed to save file: " + e.getMessage());
|
||||
}
|
||||
return "redirect:/manage/plugins/editor/" + pluginKey + "?file=" + file;
|
||||
}
|
||||
}
|
||||
+25
-35
@@ -1,23 +1,22 @@
|
||||
package com.sisvietnamvn.web.controller.manage;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Media;
|
||||
import com.sisvietnamvn.web.service.MediaService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import com.sisvietnamvn.web.security.AuthoritiesConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST controller for media file uploads (used by Editor.js and other AJAX consumers).
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/manage/media")
|
||||
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")")
|
||||
@@ -25,8 +24,11 @@ public class MediaController {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(MediaController.class);
|
||||
|
||||
// Save files in an 'uploads' directory at the project root
|
||||
private final String UPLOAD_DIR = "uploads/";
|
||||
private final MediaService mediaService;
|
||||
|
||||
public MediaController(MediaService mediaService) {
|
||||
this.mediaService = mediaService;
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<Map<String, Object>> uploadFile(@RequestParam("file") MultipartFile file) {
|
||||
@@ -40,32 +42,20 @@ public class MediaController {
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure the upload directory exists
|
||||
Path uploadPath = Paths.get(UPLOAD_DIR);
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
// Generate a unique file name to avoid conflicts
|
||||
String originalFilename = StringUtils.cleanPath(file.getOriginalFilename());
|
||||
String extension = "";
|
||||
int dotIndex = originalFilename.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
extension = originalFilename.substring(dotIndex);
|
||||
}
|
||||
String newFilename = UUID.randomUUID().toString() + extension;
|
||||
|
||||
// Save the file
|
||||
Path filePath = uploadPath.resolve(newFilename);
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
Media media = mediaService.upload(file);
|
||||
|
||||
// Construct the Editor.js expected response
|
||||
Map<String, Object> fileData = new HashMap<>();
|
||||
fileData.put("url", "/uploads/" + newFilename);
|
||||
fileData.put("name", originalFilename);
|
||||
fileData.put("size", file.getSize());
|
||||
if (extension.startsWith(".")) {
|
||||
fileData.put("extension", extension.substring(1));
|
||||
fileData.put("url", media.getFileUrl());
|
||||
fileData.put("name", media.getOriginalFilename());
|
||||
fileData.put("size", media.getFileSize());
|
||||
if (media.getMimeType() != null) {
|
||||
String ext = "";
|
||||
int dotIndex = media.getOriginalFilename().lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
ext = media.getOriginalFilename().substring(dotIndex + 1);
|
||||
}
|
||||
fileData.put("extension", ext);
|
||||
}
|
||||
|
||||
response.put("success", 1);
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import org.hibernate.annotations.Cache;
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
|
||||
/**
|
||||
* A Media entity representing an uploaded file (image, document, video, audio).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sis_media")
|
||||
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
|
||||
public class Media extends AbstractAuditingEntity<Long> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "media_sequence")
|
||||
@SequenceGenerator(name = "media_sequence", sequenceName = "sis_media_seq", allocationSize = 1)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 500)
|
||||
@Column(name = "original_filename", length = 500, nullable = false)
|
||||
private String originalFilename;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 500)
|
||||
@Column(name = "stored_filename", length = 500, nullable = false)
|
||||
private String storedFilename;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 1000)
|
||||
@Column(name = "file_url", length = 1000, nullable = false)
|
||||
private String fileUrl;
|
||||
|
||||
@Size(max = 100)
|
||||
@Column(name = "mime_type", length = 100)
|
||||
private String mimeType;
|
||||
|
||||
@Column(name = "file_size")
|
||||
private Long fileSize;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "alt_text", length = 500)
|
||||
private String altText;
|
||||
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "media_type", length = 20, nullable = false)
|
||||
private MediaType mediaType;
|
||||
|
||||
// --- Getters and Setters ---
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOriginalFilename() {
|
||||
return originalFilename;
|
||||
}
|
||||
|
||||
public void setOriginalFilename(String originalFilename) {
|
||||
this.originalFilename = originalFilename;
|
||||
}
|
||||
|
||||
public String getStoredFilename() {
|
||||
return storedFilename;
|
||||
}
|
||||
|
||||
public void setStoredFilename(String storedFilename) {
|
||||
this.storedFilename = storedFilename;
|
||||
}
|
||||
|
||||
public String getFileUrl() {
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
public void setFileUrl(String fileUrl) {
|
||||
this.fileUrl = fileUrl;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public void setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
public Long getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(Long fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public String getAltText() {
|
||||
return altText;
|
||||
}
|
||||
|
||||
public void setAltText(String altText) {
|
||||
this.altText = altText;
|
||||
}
|
||||
|
||||
public MediaType getMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
public void setMediaType(MediaType mediaType) {
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable file size string.
|
||||
*/
|
||||
public String getFormattedFileSize() {
|
||||
if (fileSize == null) return "—";
|
||||
if (fileSize < 1024) return fileSize + " B";
|
||||
if (fileSize < 1024 * 1024) return String.format("%.1f KB", fileSize / 1024.0);
|
||||
return String.format("%.1f MB", fileSize / (1024.0 * 1024.0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Media)) return false;
|
||||
return id != null && id.equals(((Media) o).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Media{" +
|
||||
"id=" + id +
|
||||
", originalFilename='" + originalFilename + '\'' +
|
||||
", storedFilename='" + storedFilename + '\'' +
|
||||
", fileUrl='" + fileUrl + '\'' +
|
||||
", mimeType='" + mimeType + '\'' +
|
||||
", fileSize=" + fileSize +
|
||||
", mediaType=" + mediaType +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
/**
|
||||
* Enum representing the type of media file.
|
||||
*/
|
||||
public enum MediaType {
|
||||
IMAGE,
|
||||
DOCUMENT,
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
OTHER
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.hibernate.annotations.Cache;
|
||||
import org.hibernate.annotations.CacheConcurrencyStrategy;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* A Plugin.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sis_plugin")
|
||||
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
|
||||
public class Plugin extends AbstractAuditingEntity<Long> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator", sequenceName = "sis_plugin_seq", allocationSize = 1)
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 100)
|
||||
@Column(name = "plugin_key", length = 100, nullable = false, unique = true)
|
||||
private String pluginKey;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 255)
|
||||
@Column(name = "name", length = 255, nullable = false)
|
||||
private String name;
|
||||
|
||||
@Size(max = 1000)
|
||||
@Column(name = "description", length = 1000)
|
||||
private String description;
|
||||
|
||||
@Size(max = 50)
|
||||
@Column(name = "version", length = 50)
|
||||
private String version;
|
||||
|
||||
@Size(max = 255)
|
||||
@Column(name = "author", length = 255)
|
||||
private String author;
|
||||
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false)
|
||||
private PluginStatus status;
|
||||
|
||||
// jhipster-needle-entity-add-field - JHipster will add fields here
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Plugin id(Long id) {
|
||||
this.setId(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getPluginKey() {
|
||||
return pluginKey;
|
||||
}
|
||||
|
||||
public void setPluginKey(String pluginKey) {
|
||||
this.pluginKey = pluginKey;
|
||||
}
|
||||
|
||||
public Plugin pluginKey(String pluginKey) {
|
||||
this.setPluginKey(pluginKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Plugin name(String name) {
|
||||
this.setName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Plugin description(String description) {
|
||||
this.setDescription(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public Plugin version(String version) {
|
||||
this.setVersion(version);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public Plugin author(String author) {
|
||||
this.setAuthor(author);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PluginStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(PluginStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Plugin status(PluginStatus status) {
|
||||
this.setStatus(status);
|
||||
return this;
|
||||
}
|
||||
|
||||
// jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Plugin)) {
|
||||
return false;
|
||||
}
|
||||
return id != null && id.equals(((Plugin) o).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Plugin{" +
|
||||
"id=" + getId() +
|
||||
", pluginKey='" + getPluginKey() + "'" +
|
||||
", name='" + getName() + "'" +
|
||||
", description='" + getDescription() + "'" +
|
||||
", version='" + getVersion() + "'" +
|
||||
", author='" + getAuthor() + "'" +
|
||||
", status='" + getStatus() + "'" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.sisvietnamvn.web.domain;
|
||||
|
||||
/**
|
||||
* The PluginStatus enumeration.
|
||||
*/
|
||||
public enum PluginStatus {
|
||||
ACTIVE, INACTIVE
|
||||
}
|
||||
+26
-6
@@ -1,7 +1,10 @@
|
||||
package com.sisvietnamvn.web.hook;
|
||||
package com.sisvietnamvn.web.plugins.sampleplugin;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Page;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import com.sisvietnamvn.web.hook.HookManager;
|
||||
import com.sisvietnamvn.web.repository.PluginRepository;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -15,13 +18,32 @@ public class SamplePluginHooks {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SamplePluginHooks.class);
|
||||
|
||||
private final HookManager hookManager;
|
||||
private final PluginRepository pluginRepository;
|
||||
private static final String PLUGIN_KEY = "sampleplugin";
|
||||
|
||||
public SamplePluginHooks(HookManager hookManager) {
|
||||
public SamplePluginHooks(HookManager hookManager, PluginRepository pluginRepository) {
|
||||
this.hookManager = hookManager;
|
||||
this.pluginRepository = pluginRepository;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void registerHooks() {
|
||||
boolean isActive = false;
|
||||
try {
|
||||
// Only register hooks if this plugin is ACTIVE in the database
|
||||
isActive = pluginRepository.findByPluginKey(PLUGIN_KEY)
|
||||
.map(p -> p.getStatus().name().equals("ACTIVE"))
|
||||
.orElse(false);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not verify plugin status (table might not exist yet due to async Liquibase): {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
LOG.info("SamplePlugin is inactive. Skipping hook registration.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Registering SamplePlugin hooks...");
|
||||
|
||||
// Example Action: Log when a page is saved
|
||||
@@ -53,8 +75,6 @@ public class SamplePluginHooks {
|
||||
if (value instanceof Page page) {
|
||||
if (page.getContent() != null && !page.getContent().contains("Auto-signature")) {
|
||||
LOG.info("FILTER TRIGGERED (pre_save_post): Appending signature to page '{}'", page.getTitle());
|
||||
// In a real scenario, this would modify the JSON content of Editor.js,
|
||||
// but for demonstration we just log it.
|
||||
}
|
||||
}
|
||||
return value;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Sample Plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "Admin",
|
||||
"description": "A sample plugin to demonstrate the HookManager."
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sisvietnamvn.web.repository;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Media;
|
||||
import com.sisvietnamvn.web.domain.MediaType;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the {@link Media} entity.
|
||||
*/
|
||||
@Repository
|
||||
public interface MediaRepository extends JpaRepository<Media, Long> {
|
||||
|
||||
List<Media> findAllByOrderByCreatedDateDesc();
|
||||
|
||||
List<Media> findByMediaTypeOrderByCreatedDateDesc(MediaType mediaType);
|
||||
|
||||
List<Media> findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(String keyword);
|
||||
|
||||
List<Media> findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(MediaType mediaType, String keyword);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sisvietnamvn.web.repository;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Plugin;
|
||||
import com.sisvietnamvn.web.domain.PluginStatus;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for the Plugin entity.
|
||||
*/
|
||||
@Repository
|
||||
public interface PluginRepository extends JpaRepository<Plugin, Long> {
|
||||
Optional<Plugin> findByPluginKey(String pluginKey);
|
||||
List<Plugin> findByStatus(PluginStatus status);
|
||||
List<Plugin> findAllByOrderByNameAsc();
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Media;
|
||||
import com.sisvietnamvn.web.domain.MediaType;
|
||||
import com.sisvietnamvn.web.repository.MediaRepository;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* Service for managing media files (upload, list, filter, delete).
|
||||
* Files are stored using a WordPress-style time-based folder structure: uploads/YYYY/MM/
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class MediaService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MediaService.class);
|
||||
|
||||
private static final String BASE_UPLOAD_DIR = "uploads";
|
||||
|
||||
private final MediaRepository mediaRepository;
|
||||
|
||||
public MediaService(MediaRepository mediaRepository) {
|
||||
this.mediaRepository = mediaRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to disk and persist a Media entity.
|
||||
* Files are stored in uploads/YYYY/MM/ based on the current date.
|
||||
*/
|
||||
public Media upload(MultipartFile file) throws IOException {
|
||||
LOG.debug("Request to upload file: {}", file.getOriginalFilename());
|
||||
|
||||
// Build WordPress-style path: uploads/YYYY/MM/
|
||||
String subPath = getUploadSubPath();
|
||||
Path uploadDir = Paths.get(BASE_UPLOAD_DIR, subPath);
|
||||
if (!Files.exists(uploadDir)) {
|
||||
Files.createDirectories(uploadDir);
|
||||
}
|
||||
|
||||
// Generate unique stored filename
|
||||
String originalFilename = StringUtils.cleanPath(file.getOriginalFilename());
|
||||
String extension = "";
|
||||
int dotIndex = originalFilename.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
extension = originalFilename.substring(dotIndex);
|
||||
}
|
||||
String storedFilename = UUID.randomUUID().toString() + extension;
|
||||
|
||||
// Save file to disk
|
||||
Path filePath = uploadDir.resolve(storedFilename);
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// Build the public URL
|
||||
String fileUrl = "/" + BASE_UPLOAD_DIR + "/" + subPath + "/" + storedFilename;
|
||||
|
||||
// Determine media type from MIME
|
||||
String mimeType = file.getContentType();
|
||||
MediaType mediaType = resolveMediaType(mimeType);
|
||||
|
||||
// Create and save entity
|
||||
Media media = new Media();
|
||||
media.setOriginalFilename(originalFilename);
|
||||
media.setStoredFilename(storedFilename);
|
||||
media.setFileUrl(fileUrl);
|
||||
media.setMimeType(mimeType);
|
||||
media.setFileSize(file.getSize());
|
||||
media.setMediaType(mediaType);
|
||||
|
||||
return mediaRepository.save(media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all media, sorted by date descending.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Media> findAll() {
|
||||
LOG.debug("Request to get all Media");
|
||||
return mediaRepository.findAllByOrderByCreatedDateDesc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find media filtered by type and/or keyword.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Media> findFiltered(MediaType type, String keyword) {
|
||||
LOG.debug("Request to get filtered Media (type={}, keyword={})", type, keyword);
|
||||
boolean hasType = type != null;
|
||||
boolean hasKeyword = keyword != null && !keyword.isBlank();
|
||||
|
||||
if (hasType && hasKeyword) {
|
||||
return mediaRepository.findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(type, keyword);
|
||||
} else if (hasType) {
|
||||
return mediaRepository.findByMediaTypeOrderByCreatedDateDesc(type);
|
||||
} else if (hasKeyword) {
|
||||
return mediaRepository.findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(keyword);
|
||||
} else {
|
||||
return mediaRepository.findAllByOrderByCreatedDateDesc();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single media by ID.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Media> findById(Long id) {
|
||||
LOG.debug("Request to get Media : {}", id);
|
||||
return mediaRepository.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a media entity and its file from disk.
|
||||
*/
|
||||
public void delete(Long id) {
|
||||
LOG.debug("Request to delete Media : {}", id);
|
||||
mediaRepository.findById(id).ifPresent(media -> {
|
||||
// Delete file from disk
|
||||
try {
|
||||
// fileUrl is like /uploads/2026/07/uuid.jpg — strip leading slash
|
||||
Path filePath = Paths.get(media.getFileUrl().substring(1));
|
||||
Files.deleteIfExists(filePath);
|
||||
LOG.debug("Deleted file from disk: {}", filePath);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to delete file from disk for media {}: {}", id, e.getMessage());
|
||||
}
|
||||
mediaRepository.delete(media);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a media entity (e.g. alt text).
|
||||
*/
|
||||
public Media save(Media media) {
|
||||
LOG.debug("Request to save Media : {}", media);
|
||||
return mediaRepository.save(media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current year/month subdirectory path (e.g. "2026/07").
|
||||
*/
|
||||
public String getUploadSubPath() {
|
||||
LocalDate now = LocalDate.now();
|
||||
return String.format("%d/%02d", now.getYear(), now.getMonthValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a MIME type string to a MediaType enum value.
|
||||
*/
|
||||
public MediaType resolveMediaType(String mimeType) {
|
||||
if (mimeType == null) return MediaType.OTHER;
|
||||
if (mimeType.startsWith("image/")) return MediaType.IMAGE;
|
||||
if (mimeType.startsWith("video/")) return MediaType.VIDEO;
|
||||
if (mimeType.startsWith("audio/")) return MediaType.AUDIO;
|
||||
if (mimeType.startsWith("application/pdf") ||
|
||||
mimeType.startsWith("application/msword") ||
|
||||
mimeType.startsWith("application/vnd.openxmlformats") ||
|
||||
mimeType.startsWith("application/vnd.ms-") ||
|
||||
mimeType.startsWith("text/")) {
|
||||
return MediaType.DOCUMENT;
|
||||
}
|
||||
return MediaType.OTHER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.sisvietnamvn.web.domain.Plugin;
|
||||
import com.sisvietnamvn.web.domain.PluginStatus;
|
||||
import com.sisvietnamvn.web.repository.PluginRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class PluginService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PluginService.class);
|
||||
|
||||
// Using the source directory so that edited/uploaded .java files can be compiled
|
||||
// and picked up by Spring Boot DevTools (or a normal build).
|
||||
private static final String PLUGIN_BASE_DIR = "src/main/java/com/sisvietnamvn/web/plugins";
|
||||
|
||||
private final PluginRepository pluginRepository;
|
||||
|
||||
public PluginService(PluginRepository pluginRepository) {
|
||||
this.pluginRepository = pluginRepository;
|
||||
ensurePluginBaseDirExists();
|
||||
}
|
||||
|
||||
private void ensurePluginBaseDirExists() {
|
||||
try {
|
||||
Path path = Paths.get(PLUGIN_BASE_DIR);
|
||||
if (!Files.exists(path)) {
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not create plugin base directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Plugin> findAll() {
|
||||
LOG.debug("Request to get all Plugins");
|
||||
return pluginRepository.findAllByOrderByNameAsc();
|
||||
}
|
||||
|
||||
public Optional<Plugin> findById(Long id) {
|
||||
LOG.debug("Request to get Plugin : {}", id);
|
||||
return pluginRepository.findById(id);
|
||||
}
|
||||
|
||||
public Optional<Plugin> findByKey(String key) {
|
||||
return pluginRepository.findByPluginKey(key);
|
||||
}
|
||||
|
||||
public boolean isPluginActive(String key) {
|
||||
return pluginRepository.findByPluginKey(key)
|
||||
.map(plugin -> plugin.getStatus() == PluginStatus.ACTIVE)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
public Plugin activate(Long id) {
|
||||
return pluginRepository.findById(id).map(plugin -> {
|
||||
plugin.setStatus(PluginStatus.ACTIVE);
|
||||
LOG.info("Activated plugin: {}", plugin.getName());
|
||||
return plugin;
|
||||
}).orElseThrow(() -> new IllegalArgumentException("Plugin not found"));
|
||||
}
|
||||
|
||||
public Plugin deactivate(Long id) {
|
||||
return pluginRepository.findById(id).map(plugin -> {
|
||||
plugin.setStatus(PluginStatus.INACTIVE);
|
||||
LOG.info("Deactivated plugin: {}", plugin.getName());
|
||||
return plugin;
|
||||
}).orElseThrow(() -> new IllegalArgumentException("Plugin not found"));
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
pluginRepository.findById(id).ifPresent(plugin -> {
|
||||
String key = plugin.getPluginKey();
|
||||
pluginRepository.delete(plugin);
|
||||
LOG.info("Deleted plugin from DB: {}", key);
|
||||
|
||||
// Also delete files
|
||||
Path pluginDir = Paths.get(PLUGIN_BASE_DIR, key);
|
||||
if (Files.exists(pluginDir)) {
|
||||
try {
|
||||
Files.walkFileTree(pluginDir, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
LOG.info("Deleted plugin directory: {}", pluginDir);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to delete plugin directory: {}", pluginDir, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void installFromZip(MultipartFile zipFile) throws IOException {
|
||||
String originalFilename = zipFile.getOriginalFilename();
|
||||
if (originalFilename == null || !originalFilename.endsWith(".zip")) {
|
||||
throw new IllegalArgumentException("File must be a .zip archive");
|
||||
}
|
||||
|
||||
String pluginKey = originalFilename.substring(0, originalFilename.length() - 4).replaceAll("[^a-zA-Z0-9-]", "-").toLowerCase();
|
||||
Path pluginDir = Paths.get(PLUGIN_BASE_DIR, pluginKey);
|
||||
|
||||
if (Files.exists(pluginDir)) {
|
||||
throw new IllegalArgumentException("Plugin directory already exists: " + pluginKey);
|
||||
}
|
||||
|
||||
Files.createDirectories(pluginDir);
|
||||
|
||||
// Extract ZIP
|
||||
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
|
||||
ZipEntry zipEntry = zis.getNextEntry();
|
||||
while (zipEntry != null) {
|
||||
Path newFile = newFile(pluginDir, zipEntry);
|
||||
if (zipEntry.isDirectory()) {
|
||||
Files.createDirectories(newFile);
|
||||
} else {
|
||||
// Create parent directories if needed
|
||||
Path parent = newFile.getParent();
|
||||
if (!Files.exists(parent)) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
// Write file
|
||||
Files.copy(zis, newFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
zipEntry = zis.getNextEntry();
|
||||
}
|
||||
zis.closeEntry();
|
||||
}
|
||||
|
||||
// Create a basic DB entry
|
||||
Plugin plugin = new Plugin();
|
||||
plugin.setPluginKey(pluginKey);
|
||||
plugin.setName(pluginKey);
|
||||
plugin.setVersion("1.0");
|
||||
plugin.setStatus(PluginStatus.INACTIVE);
|
||||
plugin.setAuthor("Uploaded");
|
||||
plugin.setDescription("Custom uploaded plugin");
|
||||
pluginRepository.save(plugin);
|
||||
|
||||
LOG.info("Installed new plugin from zip: {}", pluginKey);
|
||||
}
|
||||
|
||||
private Path newFile(Path destinationDir, ZipEntry zipEntry) throws IOException {
|
||||
Path destFile = destinationDir.resolve(zipEntry.getName());
|
||||
String destDirPath = destinationDir.toFile().getCanonicalPath();
|
||||
String destFilePath = destFile.toFile().getCanonicalPath();
|
||||
|
||||
if (!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
|
||||
}
|
||||
return destFile;
|
||||
}
|
||||
|
||||
// --- Plugin File Editor methods ---
|
||||
|
||||
public List<String> getPluginFiles(String pluginKey) throws IOException {
|
||||
Path pluginDir = Paths.get(PLUGIN_BASE_DIR, pluginKey);
|
||||
List<String> files = new ArrayList<>();
|
||||
if (Files.exists(pluginDir)) {
|
||||
Files.walkFileTree(pluginDir, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
files.add(pluginDir.relativize(file).toString().replace("\\", "/"));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
public String readPluginFile(String pluginKey, String filePath) throws IOException {
|
||||
Path targetPath = resolveAndCheckPath(pluginKey, filePath);
|
||||
if (Files.exists(targetPath) && !Files.isDirectory(targetPath)) {
|
||||
return Files.readString(targetPath);
|
||||
}
|
||||
throw new IllegalArgumentException("File not found or is a directory: " + filePath);
|
||||
}
|
||||
|
||||
public void writePluginFile(String pluginKey, String filePath, String content) throws IOException {
|
||||
Path targetPath = resolveAndCheckPath(pluginKey, filePath);
|
||||
if (Files.exists(targetPath) && !Files.isDirectory(targetPath)) {
|
||||
Files.writeString(targetPath, content, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
LOG.info("Saved changes to plugin file: {}/{}", pluginKey, filePath);
|
||||
} else {
|
||||
throw new IllegalArgumentException("File not found or is a directory: " + filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveAndCheckPath(String pluginKey, String filePath) throws IOException {
|
||||
Path pluginDir = Paths.get(PLUGIN_BASE_DIR, pluginKey).toAbsolutePath().normalize();
|
||||
Path targetPath = pluginDir.resolve(filePath).normalize();
|
||||
if (!targetPath.startsWith(pluginDir)) {
|
||||
throw new SecurityException("Path traversal attempt detected!");
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user