mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
Save signatures to server (#5080)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
fde449e738
commit
651f17f1c6
@@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.SPDF.service.SharedSignatureService;
|
||||
import stirling.software.common.annotations.api.UiDataApi;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
@@ -40,14 +40,14 @@ import stirling.software.common.util.GeneralUtils;
|
||||
public class UIDataController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final SignatureService signatureService;
|
||||
private final SharedSignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public UIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SignatureService signatureService,
|
||||
SharedSignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
|
||||
+3
-3
@@ -25,7 +25,7 @@ import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.SPDF.service.SharedSignatureService;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
@@ -37,13 +37,13 @@ import stirling.software.common.util.GeneralUtils;
|
||||
@Slf4j
|
||||
public class GeneralWebController {
|
||||
|
||||
private final SignatureService signatureService;
|
||||
private final SharedSignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public GeneralWebController(
|
||||
SignatureService signatureService,
|
||||
SharedSignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
|
||||
// @Controller // Disabled - Backend-only mode, no Thymeleaf UI
|
||||
@RequestMapping("/api/v1/general")
|
||||
public class SignatureController {
|
||||
|
||||
private final SignatureService signatureService;
|
||||
|
||||
private final UserServiceInterface userService;
|
||||
|
||||
public SignatureController(
|
||||
SignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService) {
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/sign/{fileName}")
|
||||
public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName)
|
||||
throws IOException {
|
||||
String username = "NON_SECURITY_USER";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
// Verify access permission
|
||||
if (!signatureService.hasAccessToFile(username, fileName)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
byte[] imageBytes = signatureService.getSignatureBytes(username, fileName);
|
||||
return ResponseEntity.ok()
|
||||
.contentType( // Adjust based on file type
|
||||
MediaType.IMAGE_JPEG)
|
||||
.body(imageBytes);
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.service.SharedSignatureService;
|
||||
import stirling.software.common.service.PersonalSignatureServiceInterface;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
|
||||
/**
|
||||
* Unified signature image controller that works for both authenticated and unauthenticated users.
|
||||
* Uses composition pattern: - Core SharedSignatureService (always available): reads shared signatures -
|
||||
* PersonalSignatureService (proprietary, optional): reads personal signatures For authenticated
|
||||
* signature management (save/delete), see proprietary SignatureController.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
public class SignatureImageController {
|
||||
|
||||
private final SharedSignatureService sharedSignatureService;
|
||||
private final PersonalSignatureServiceInterface personalSignatureService;
|
||||
private final UserServiceInterface userService;
|
||||
|
||||
public SignatureImageController(
|
||||
SharedSignatureService sharedSignatureService,
|
||||
@Autowired(required = false) PersonalSignatureServiceInterface personalSignatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService) {
|
||||
this.sharedSignatureService = sharedSignatureService;
|
||||
this.personalSignatureService = personalSignatureService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signature image (works for both authenticated and unauthenticated users). -
|
||||
* Authenticated with proprietary: tries personal first, then shared - Unauthenticated or
|
||||
* community: tries shared only
|
||||
*/
|
||||
@GetMapping("/signatures/{fileName}")
|
||||
public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName) {
|
||||
try {
|
||||
byte[] imageBytes = null;
|
||||
|
||||
// If proprietary service available and user authenticated, try personal folder first
|
||||
if (personalSignatureService != null && userService != null) {
|
||||
try {
|
||||
String username = userService.getCurrentUsername();
|
||||
imageBytes =
|
||||
personalSignatureService.getPersonalSignatureBytes(username, fileName);
|
||||
} catch (Exception e) {
|
||||
// Not found in personal folder or not authenticated, will try shared
|
||||
log.debug("Personal signature not found, trying shared: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in personal (or no personal service), try shared
|
||||
if (imageBytes == null) {
|
||||
imageBytes = sharedSignatureService.getSharedSignatureBytes(fileName);
|
||||
}
|
||||
|
||||
// Determine content type from file extension
|
||||
MediaType contentType = MediaType.IMAGE_PNG; // Default
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
if (lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg")) {
|
||||
contentType = MediaType.IMAGE_JPEG;
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().contentType(contentType).body(imageBytes);
|
||||
} catch (IOException e) {
|
||||
log.debug("Signature not found: {}", fileName);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package stirling.software.SPDF.model.api.signature;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class SavedSignatureRequest {
|
||||
private String id;
|
||||
private String label;
|
||||
private String type; // "canvas", "image", "text"
|
||||
private String scope; // "personal", "shared"
|
||||
private String dataUrl; // For canvas and image types
|
||||
private String signerName; // For text type
|
||||
private String fontFamily; // For text type
|
||||
private Integer fontSize; // For text type
|
||||
private String textColor; // For text type
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package stirling.software.SPDF.model.api.signature;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SavedSignatureResponse {
|
||||
private String id;
|
||||
private String label;
|
||||
private String type; // "canvas", "image", "text"
|
||||
private String scope; // "personal", "shared"
|
||||
private String dataUrl; // For canvas and image types (or URL to fetch image)
|
||||
private String signerName; // For text type
|
||||
private String fontFamily; // For text type
|
||||
private Integer fontSize; // For text type
|
||||
private String textColor; // For text type
|
||||
private Long createdAt;
|
||||
private Long updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.model.api.signature.SavedSignatureRequest;
|
||||
import stirling.software.SPDF.model.api.signature.SavedSignatureResponse;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SharedSignatureService {
|
||||
|
||||
private final String SIGNATURE_BASE_PATH;
|
||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SharedSignatureService() {
|
||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||
validateFileName(fileName);
|
||||
// Check if file exists in user's personal folder or ALL_USERS folder
|
||||
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
|
||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||
|
||||
return Files.exists(userPath) || Files.exists(allUsersPath);
|
||||
}
|
||||
|
||||
public List<SignatureFile> getAvailableSignatures(String username) {
|
||||
List<SignatureFile> signatures = new ArrayList<>();
|
||||
|
||||
// Get signatures from user's personal folder
|
||||
if (StringUtils.hasText(username)) {
|
||||
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
||||
if (Files.exists(userFolder)) {
|
||||
try {
|
||||
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading user signatures folder", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get signatures from ALL_USERS folder
|
||||
Path allUsersFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||
if (Files.exists(allUsersFolder)) {
|
||||
try {
|
||||
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading shared signatures folder", e);
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
private List<SignatureFile> getSignaturesFromFolder(Path folder, String category)
|
||||
throws IOException {
|
||||
try (Stream<Path> stream = Files.list(folder)) {
|
||||
return stream.filter(this::isImageFile)
|
||||
.map(path -> new SignatureFile(path.getFileName().toString(), category))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signature from the shared (ALL_USERS) folder. This is always available for both
|
||||
* authenticated and unauthenticated users.
|
||||
*/
|
||||
public byte[] getSharedSignatureBytes(String fileName) throws IOException {
|
||||
validateFileName(fileName);
|
||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||
if (!Files.exists(allUsersPath)) {
|
||||
throw new FileNotFoundException("Shared signature file not found");
|
||||
}
|
||||
return Files.readAllBytes(allUsersPath);
|
||||
}
|
||||
|
||||
private boolean isImageFile(Path path) {
|
||||
String fileName = path.getFileName().toString().toLowerCase();
|
||||
return fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || fileName.endsWith(".png");
|
||||
}
|
||||
|
||||
private void validateFileName(String fileName) {
|
||||
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
||||
throw new IllegalArgumentException("Invalid filename");
|
||||
}
|
||||
// Only allow alphanumeric, hyphen, underscore, and dot (for extensions)
|
||||
if (!fileName.matches("^[a-zA-Z0-9_.-]+$")) {
|
||||
throw new IllegalArgumentException("Filename contains invalid characters");
|
||||
}
|
||||
}
|
||||
|
||||
private String validateAndNormalizeExtension(String extension) {
|
||||
String normalized = extension.toLowerCase().trim();
|
||||
// Whitelist only safe image extensions
|
||||
if (normalized.equals("png") || normalized.equals("jpg") || normalized.equals("jpeg")) {
|
||||
return normalized;
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported image extension: " + extension);
|
||||
}
|
||||
|
||||
private void verifyPathWithinDirectory(Path resolvedPath, Path targetDirectory)
|
||||
throws IOException {
|
||||
Path canonicalTarget = targetDirectory.toAbsolutePath().normalize();
|
||||
Path canonicalResolved = resolvedPath.toAbsolutePath().normalize();
|
||||
if (!canonicalResolved.startsWith(canonicalTarget)) {
|
||||
throw new IOException("Resolved path is outside the target directory");
|
||||
}
|
||||
}
|
||||
|
||||
/** Save a signature as image file */
|
||||
public SavedSignatureResponse saveSignature(String username, SavedSignatureRequest request)
|
||||
throws IOException {
|
||||
validateFileName(request.getId());
|
||||
|
||||
// Determine folder based on scope
|
||||
String scope = request.getScope();
|
||||
if (scope == null || scope.isEmpty()) {
|
||||
scope = "personal"; // Default to personal
|
||||
}
|
||||
|
||||
String folderName = "shared".equals(scope) ? ALL_USERS_FOLDER : username;
|
||||
Path targetFolder = Paths.get(SIGNATURE_BASE_PATH, folderName);
|
||||
Files.createDirectories(targetFolder);
|
||||
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
SavedSignatureResponse response = new SavedSignatureResponse();
|
||||
response.setId(request.getId());
|
||||
response.setLabel(request.getLabel());
|
||||
response.setType(request.getType());
|
||||
response.setScope(scope);
|
||||
response.setCreatedAt(timestamp);
|
||||
response.setUpdatedAt(timestamp);
|
||||
|
||||
// Extract and save image data
|
||||
String dataUrl = request.getDataUrl();
|
||||
if (dataUrl != null && dataUrl.startsWith("data:image/")) {
|
||||
// Extract base64 data
|
||||
String base64Data = dataUrl.substring(dataUrl.indexOf(",") + 1);
|
||||
byte[] imageBytes = Base64.getDecoder().decode(base64Data);
|
||||
|
||||
// Determine and validate file extension from data URL
|
||||
String mimeType = dataUrl.substring(dataUrl.indexOf(":") + 1, dataUrl.indexOf(";"));
|
||||
String rawExtension = mimeType.substring(mimeType.indexOf("/") + 1);
|
||||
String extension = validateAndNormalizeExtension(rawExtension);
|
||||
|
||||
// Save image file only
|
||||
String imageFileName = request.getId() + "." + extension;
|
||||
Path imagePath = targetFolder.resolve(imageFileName);
|
||||
|
||||
// Verify path is within target directory
|
||||
verifyPathWithinDirectory(imagePath, targetFolder);
|
||||
|
||||
Files.write(
|
||||
imagePath,
|
||||
imageBytes,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
|
||||
// Store reference to image file
|
||||
response.setDataUrl("/api/v1/general/sign/" + imageFileName);
|
||||
}
|
||||
|
||||
log.info("Saved signature {} for user {}", request.getId(), username);
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Get all saved signatures for a user */
|
||||
public List<SavedSignatureResponse> getSavedSignatures(String username) throws IOException {
|
||||
List<SavedSignatureResponse> signatures = new ArrayList<>();
|
||||
|
||||
// Load personal signatures
|
||||
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
||||
if (Files.exists(personalFolder)) {
|
||||
try (Stream<Path> stream = Files.list(personalFolder)) {
|
||||
stream.filter(this::isImageFile)
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
String fileName = path.getFileName().toString();
|
||||
String id =
|
||||
fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
|
||||
SavedSignatureResponse sig = new SavedSignatureResponse();
|
||||
sig.setId(id);
|
||||
sig.setLabel(id); // Use ID as label
|
||||
sig.setType("image"); // Default type
|
||||
sig.setScope("personal");
|
||||
sig.setDataUrl("/api/v1/general/sign/" + fileName);
|
||||
sig.setCreatedAt(
|
||||
Files.getLastModifiedTime(path).toMillis());
|
||||
sig.setUpdatedAt(
|
||||
Files.getLastModifiedTime(path).toMillis());
|
||||
|
||||
signatures.add(sig);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading signature file: " + path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load shared signatures
|
||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||
if (Files.exists(sharedFolder)) {
|
||||
try (Stream<Path> stream = Files.list(sharedFolder)) {
|
||||
stream.filter(this::isImageFile)
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
String fileName = path.getFileName().toString();
|
||||
String id =
|
||||
fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
|
||||
SavedSignatureResponse sig = new SavedSignatureResponse();
|
||||
sig.setId(id);
|
||||
sig.setLabel(id); // Use ID as label
|
||||
sig.setType("image"); // Default type
|
||||
sig.setScope("shared");
|
||||
sig.setDataUrl("/api/v1/general/sign/" + fileName);
|
||||
sig.setCreatedAt(
|
||||
Files.getLastModifiedTime(path).toMillis());
|
||||
sig.setUpdatedAt(
|
||||
Files.getLastModifiedTime(path).toMillis());
|
||||
|
||||
signatures.add(sig);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading signature file: " + path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
/** Delete a saved signature */
|
||||
public void deleteSignature(String username, String signatureId) throws IOException {
|
||||
validateFileName(signatureId);
|
||||
|
||||
// Try to find and delete image file in personal folder
|
||||
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
||||
boolean deleted = false;
|
||||
|
||||
if (Files.exists(personalFolder)) {
|
||||
try (Stream<Path> stream = Files.list(personalFolder)) {
|
||||
List<Path> matchingFiles =
|
||||
stream.filter(
|
||||
path ->
|
||||
path.getFileName()
|
||||
.toString()
|
||||
.startsWith(signatureId + "."))
|
||||
.toList();
|
||||
for (Path file : matchingFiles) {
|
||||
Files.delete(file);
|
||||
deleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try shared folder if not found in personal
|
||||
if (!deleted) {
|
||||
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||
if (Files.exists(sharedFolder)) {
|
||||
try (Stream<Path> stream = Files.list(sharedFolder)) {
|
||||
List<Path> matchingFiles =
|
||||
stream.filter(
|
||||
path ->
|
||||
path.getFileName()
|
||||
.toString()
|
||||
.startsWith(signatureId + "."))
|
||||
.toList();
|
||||
for (Path file : matchingFiles) {
|
||||
Files.delete(file);
|
||||
deleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!deleted) {
|
||||
throw new FileNotFoundException("Signature not found");
|
||||
}
|
||||
|
||||
log.info("Deleted signature {} for user {}", signatureId, username);
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SignatureService {
|
||||
|
||||
private final String SIGNATURE_BASE_PATH;
|
||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
|
||||
public SignatureService() {
|
||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||
}
|
||||
|
||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||
validateFileName(fileName);
|
||||
// Check if file exists in user's personal folder or ALL_USERS folder
|
||||
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
|
||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||
|
||||
return Files.exists(userPath) || Files.exists(allUsersPath);
|
||||
}
|
||||
|
||||
public List<SignatureFile> getAvailableSignatures(String username) {
|
||||
List<SignatureFile> signatures = new ArrayList<>();
|
||||
|
||||
// Get signatures from user's personal folder
|
||||
if (StringUtils.hasText(username)) {
|
||||
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
|
||||
if (Files.exists(userFolder)) {
|
||||
try {
|
||||
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading user signatures folder", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get signatures from ALL_USERS folder
|
||||
Path allUsersFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
|
||||
if (Files.exists(allUsersFolder)) {
|
||||
try {
|
||||
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading shared signatures folder", e);
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
private List<SignatureFile> getSignaturesFromFolder(Path folder, String category)
|
||||
throws IOException {
|
||||
try (Stream<Path> stream = Files.list(folder)) {
|
||||
return stream.filter(this::isImageFile)
|
||||
.map(path -> new SignatureFile(path.getFileName().toString(), category))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getSignatureBytes(String username, String fileName) throws IOException {
|
||||
validateFileName(fileName);
|
||||
// First try user's personal folder
|
||||
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
|
||||
if (Files.exists(userPath)) {
|
||||
return Files.readAllBytes(userPath);
|
||||
}
|
||||
|
||||
// Then try ALL_USERS folder
|
||||
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
|
||||
if (Files.exists(allUsersPath)) {
|
||||
return Files.readAllBytes(allUsersPath);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException("Signature file not found");
|
||||
}
|
||||
|
||||
private boolean isImageFile(Path path) {
|
||||
String fileName = path.getFileName().toString().toLowerCase();
|
||||
return fileName.endsWith(".jpg")
|
||||
|| fileName.endsWith(".jpeg")
|
||||
|| fileName.endsWith(".png")
|
||||
|| fileName.endsWith(".gif");
|
||||
}
|
||||
|
||||
private void validateFileName(String fileName) {
|
||||
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
||||
throw new IllegalArgumentException("Invalid filename");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user