diff --git a/backend/src/main/java/com/storycove/service/DatabaseManagementService.java b/backend/src/main/java/com/storycove/service/DatabaseManagementService.java index 44446f0..547930a 100644 --- a/backend/src/main/java/com/storycove/service/DatabaseManagementService.java +++ b/backend/src/main/java/com/storycove/service/DatabaseManagementService.java @@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -141,26 +140,48 @@ public class DatabaseManagementService implements ApplicationContextAware { /** * Create a comprehensive backup including database and files in ZIP format + * Returns a streaming resource to avoid loading large backups into memory */ public Resource createCompleteBackup() throws SQLException, IOException { + // Create temp file with deleteOnExit as safety net Path tempZip = Files.createTempFile("storycove-backup", ".zip"); - + tempZip.toFile().deleteOnExit(); + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(tempZip))) { // 1. Add database dump addDatabaseDumpToZip(zipOut); - + // 2. Add all image files addFilesToZip(zipOut); - + // 3. Add metadata addMetadataToZip(zipOut); } - - // Return the ZIP file as a resource - byte[] zipData = Files.readAllBytes(tempZip); - Files.deleteIfExists(tempZip); - - return new ByteArrayResource(zipData); + + // Return the ZIP file as a FileSystemResource for streaming + // This avoids loading the entire file into memory + return new org.springframework.core.io.FileSystemResource(tempZip.toFile()) { + @Override + public InputStream getInputStream() throws IOException { + // Wrap the input stream to delete the temp file after it's fully read + return new java.io.FilterInputStream(super.getInputStream()) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + // Clean up temp file after streaming is complete + try { + Files.deleteIfExists(tempZip); + } catch (IOException e) { + // Log but don't fail - deleteOnExit will handle it + System.err.println("Warning: Could not delete temp backup file: " + e.getMessage()); + } + } + } + }; + } + }; } /** @@ -289,20 +310,34 @@ public class DatabaseManagementService implements ApplicationContextAware { System.err.println("PostgreSQL backup completed successfully"); - // Read the backup file into memory - byte[] backupData = Files.readAllBytes(tempBackupFile); - return new ByteArrayResource(backupData); + // Return the backup file as a streaming resource to avoid memory issues with large databases + tempBackupFile.toFile().deleteOnExit(); + return new org.springframework.core.io.FileSystemResource(tempBackupFile.toFile()) { + @Override + public InputStream getInputStream() throws IOException { + // Wrap the input stream to delete the temp file after it's fully read + return new java.io.FilterInputStream(super.getInputStream()) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + // Clean up temp file after streaming is complete + try { + Files.deleteIfExists(tempBackupFile); + } catch (IOException e) { + // Log but don't fail - deleteOnExit will handle it + System.err.println("Warning: Could not delete temp backup file: " + e.getMessage()); + } + } + } + }; + } + }; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Backup process was interrupted", e); - } finally { - // Clean up temporary file - try { - Files.deleteIfExists(tempBackupFile); - } catch (IOException e) { - System.err.println("Warning: Could not delete temporary backup file: " + e.getMessage()); - } } }