|
2 | 2 |
|
3 | 3 | import java.io.File; |
4 | 4 | import java.io.IOException; |
| 5 | +import java.io.OutputStream; |
| 6 | +import java.nio.file.Files; |
| 7 | +import java.nio.file.StandardCopyOption; |
5 | 8 | import java.time.Duration; |
6 | 9 | import java.time.Instant; |
7 | 10 | import java.time.format.DateTimeFormatter; |
8 | | -import java.util.Collections; |
| 11 | +import java.util.Arrays; |
9 | 12 | import java.util.List; |
10 | | -import java.util.Optional; |
11 | 13 |
|
12 | 14 | import org.springframework.beans.factory.annotation.Value; |
| 15 | +import org.springframework.core.io.Resource; |
| 16 | +import org.springframework.core.io.ResourceLoader; |
| 17 | +import org.springframework.core.io.WritableResource; |
13 | 18 | import org.springframework.stereotype.Service; |
14 | 19 |
|
| 20 | +import com.azure.spring.cloud.core.resource.AzureStorageBlobProtocolResolver; |
15 | 21 | import com.azure.storage.blob.BlobContainerClient; |
16 | | -import com.azure.storage.blob.BlobServiceClient; |
17 | | -import com.azure.storage.blob.models.BlobItem; |
18 | | -import com.azure.storage.blob.models.ListBlobsOptions; |
19 | 22 |
|
20 | 23 | import io.github.mucsi96.postgresbackuptool.model.Backup; |
21 | 24 | import lombok.extern.slf4j.Slf4j; |
22 | 25 |
|
23 | 26 | @Service |
24 | 27 | @Slf4j |
25 | 28 | public class BackupService { |
26 | | - private final BlobServiceClient blobServiceClient; |
27 | 29 | private final DateTimeFormatter dateTimeFormatter; |
28 | 30 | private final String containerName; |
29 | | - |
30 | | - public BackupService(BlobServiceClient blobServiceClient, |
31 | | - DateTimeFormatter dateTimeFormatter, |
32 | | - @Value("${blobstorage.containerName}") String containerName) { |
33 | | - this.blobServiceClient = blobServiceClient; |
| 31 | + private final ResourceLoader resourceLoader; |
| 32 | + private final AzureStorageBlobProtocolResolver azureStorageBlobProtocolResolver; |
| 33 | + private final BlobContainerClient blobContainerClient; |
| 34 | + |
| 35 | + public BackupService(DateTimeFormatter dateTimeFormatter, |
| 36 | + @Value("${spring.cloud.azure.storage.blob.container-name}") String containerName, |
| 37 | + ResourceLoader resourceLoader, |
| 38 | + AzureStorageBlobProtocolResolver azureStorageBlobProtocolResolver, |
| 39 | + BlobContainerClient blobContainerClient) { |
34 | 40 | this.dateTimeFormatter = dateTimeFormatter; |
35 | 41 | this.containerName = containerName; |
| 42 | + this.resourceLoader = resourceLoader; |
| 43 | + this.azureStorageBlobProtocolResolver = azureStorageBlobProtocolResolver; |
| 44 | + this.blobContainerClient = blobContainerClient; |
36 | 45 | } |
37 | 46 |
|
38 | 47 | public List<Backup> getBackups(String prefix) { |
39 | | - return Optional.of(blobServiceClient.getBlobContainerClient(containerName)) |
40 | | - .filter(BlobContainerClient::exists) |
41 | | - .map(container -> listAndTransformBlobs(container, prefix)) |
42 | | - .orElse(Collections.emptyList()); |
43 | | - } |
44 | | - |
45 | | - private List<Backup> listAndTransformBlobs(BlobContainerClient container, String prefix) { |
46 | | - return container |
47 | | - .listBlobs(new ListBlobsOptions().setPrefix(prefix + "/"), null) |
48 | | - .stream() |
49 | | - .filter(blob -> blob.getName().endsWith(".zip")) // Only ZIP files |
50 | | - .map(blob -> createBackupFromBlob(blob, prefix)) |
51 | | - .sorted((a, b) -> b.getLastModified().compareTo(a.getLastModified())) |
52 | | - .toList(); |
| 48 | + try { |
| 49 | + Resource[] resources = azureStorageBlobProtocolResolver |
| 50 | + .getResources(String.format("azure-blob://%s/%s/*.zip", |
| 51 | + containerName, prefix)); |
| 52 | + return Arrays.stream(resources) |
| 53 | + .map(resource -> createBackupFromResource(resource)) |
| 54 | + .sorted((a, b) -> b.getLastModified() |
| 55 | + .compareTo(a.getLastModified())) |
| 56 | + .toList(); |
| 57 | + } catch (IOException e) { |
| 58 | + log.error("Failed to list backups", e); |
| 59 | + return List.of(); |
| 60 | + } |
53 | 61 | } |
54 | 62 |
|
55 | | - private Backup createBackupFromBlob(BlobItem blob, String prefix) { |
56 | | - String name = getBackupName(prefix, blob); |
57 | | - |
58 | | - // Parse metadata from filename |
59 | | - return Backup.builder() |
60 | | - .name(name) |
61 | | - .lastModified(parseBackupTimestamp(name)) |
62 | | - .size(blob.getProperties().getContentLength()) |
63 | | - .totalRowCount(getTotalCountFromName(prefix, blob)) |
64 | | - .fileCount(getFileCountFromName(prefix, blob)) |
65 | | - .filesTotalSize(getFilesTotalSizeFromName(prefix, blob)) |
66 | | - .retentionPeriod(getRetentionPeriodFromName(prefix, blob)) |
67 | | - .hasPlainDump(true) |
68 | | - .build(); |
| 63 | + private Backup createBackupFromResource(Resource resource) { |
| 64 | + String name = resource.getFilename(); |
| 65 | + try { |
| 66 | + return Backup.builder().name(name) |
| 67 | + .lastModified(parseBackupTimestamp(name)) |
| 68 | + .size(resource.contentLength()) |
| 69 | + .totalRowCount(getTotalCountFromName(name)) |
| 70 | + .fileCount(getFileCountFromName(name)) |
| 71 | + .filesTotalSize(getFilesTotalSizeFromName(name)) |
| 72 | + .retentionPeriod(getRetentionPeriodFromName(name)) |
| 73 | + .hasPlainDump(true).build(); |
| 74 | + } catch (IOException e) { |
| 75 | + throw new RuntimeException("Failed to get resource content length", |
| 76 | + e); |
| 77 | + } |
69 | 78 | } |
70 | 79 |
|
71 | 80 | private Instant parseBackupTimestamp(String name) { |
72 | 81 | return dateTimeFormatter.parse(name.substring(0, 15), Instant::from); |
73 | 82 | } |
74 | 83 |
|
75 | | - public void createBackup(String prefix, File dumpFile, String fileName) { |
76 | | - BlobContainerClient blobContainerClient = blobServiceClient |
77 | | - .getBlobContainerClient(containerName); |
78 | | - |
79 | | - blobContainerClient.getBlobClient(prefix + "/" + fileName) |
80 | | - .uploadFromFile(dumpFile.getAbsolutePath()); |
| 84 | + public void createBackup(String prefix, File dumpFile, String fileName) |
| 85 | + throws IOException { |
| 86 | + String location = String.format("azure-blob://%s/%s/%s", containerName, |
| 87 | + prefix, fileName); |
| 88 | + WritableResource resource = (WritableResource) resourceLoader |
| 89 | + .getResource(location); |
| 90 | + try (OutputStream os = resource.getOutputStream()) { |
| 91 | + Files.copy(dumpFile.toPath(), os); |
| 92 | + } |
81 | 93 | } |
82 | 94 |
|
83 | 95 | public File downloadBackup(String prefix, String key) throws IOException { |
84 | | - BlobContainerClient blobContainerClient = blobServiceClient |
85 | | - .getBlobContainerClient(containerName); |
86 | | - |
87 | | - blobContainerClient.getBlobClient(prefix + "/" + key) |
88 | | - .downloadToFile(key); |
89 | | - |
90 | | - return new File(key); |
| 96 | + String location = String.format("azure-blob://%s/%s/%s", containerName, |
| 97 | + prefix, key); |
| 98 | + Resource resource = resourceLoader.getResource(location); |
| 99 | + File file = new File(key); |
| 100 | + Files.copy(resource.getInputStream(), file.toPath(), |
| 101 | + StandardCopyOption.REPLACE_EXISTING); |
| 102 | + return file; |
91 | 103 | } |
92 | 104 |
|
93 | 105 | public void cleanup(String prefix) { |
94 | | - BlobContainerClient blobContainerClient = blobServiceClient |
95 | | - .getBlobContainerClient(containerName); |
96 | | - |
97 | | - blobContainerClient |
98 | | - .listBlobs(new ListBlobsOptions().setPrefix(prefix + "/"), null) |
99 | | - .stream().filter(blobItem -> shouldCleanup(prefix, blobItem)) |
100 | | - .forEach(blobItem -> blobContainerClient |
101 | | - .getBlobClient(blobItem.getName()).delete()); |
| 106 | + getBackups(prefix).stream().filter(this::shouldCleanup) |
| 107 | + .forEach(backup -> blobContainerClient |
| 108 | + .getBlobClient(prefix + "/" + backup.getName()) |
| 109 | + .delete()); |
102 | 110 | } |
103 | 111 |
|
104 | | - public Optional<Instant> getLastBackupTime(String prefix) { |
| 112 | + public java.util.Optional<Instant> getLastBackupTime(String prefix) { |
105 | 113 | return getBackups(prefix).stream().findFirst() |
106 | 114 | .map(Backup::getLastModified); |
107 | 115 | } |
108 | 116 |
|
109 | | - private int getTotalCountFromName(String prefix, BlobItem backup) { |
110 | | - return Integer.parseInt(getBackupName(prefix, backup).split("\\.")[1]); |
| 117 | + private int getTotalCountFromName(String name) { |
| 118 | + return Integer.parseInt(name.split("\\.")[1]); |
111 | 119 | } |
112 | 120 |
|
113 | | - private int getFileCountFromName(String prefix, BlobItem backup) { |
114 | | - return Integer.parseInt(getBackupName(prefix, backup).split("\\.")[2]); |
| 121 | + private int getFileCountFromName(String name) { |
| 122 | + return Integer.parseInt(name.split("\\.")[2]); |
115 | 123 | } |
116 | 124 |
|
117 | | - private long getFilesTotalSizeFromName(String prefix, BlobItem backup) { |
118 | | - return Long.parseLong(getBackupName(prefix, backup).split("\\.")[3]); |
| 125 | + private long getFilesTotalSizeFromName(String name) { |
| 126 | + return Long.parseLong(name.split("\\.")[3]); |
119 | 127 | } |
120 | 128 |
|
121 | | - private int getRetentionPeriodFromName(String prefix, BlobItem backup) { |
122 | | - return Integer.parseInt(getBackupName(prefix, backup).split("\\.")[4]); |
| 129 | + private int getRetentionPeriodFromName(String name) { |
| 130 | + return Integer.parseInt(name.split("\\.")[4]); |
123 | 131 | } |
124 | 132 |
|
125 | | - private boolean shouldCleanup(String prefix, BlobItem backup) { |
126 | | - Backup b = Backup.builder().name(backup.getName()) |
127 | | - .lastModified(dateTimeFormatter.parse( |
128 | | - getBackupName(prefix, backup).substring(0, 15), |
129 | | - Instant::from)) |
130 | | - .retentionPeriod(getRetentionPeriodFromName(prefix, backup)) |
131 | | - .build(); |
132 | | - Instant cleanupDate = b.getLastModified() |
133 | | - .plus(Duration.ofDays(b.getRetentionPeriod())); |
| 133 | + private boolean shouldCleanup(Backup backup) { |
| 134 | + Instant cleanupDate = backup.getLastModified() |
| 135 | + .plus(Duration.ofDays(backup.getRetentionPeriod())); |
134 | 136 |
|
135 | 137 | return cleanupDate.isBefore(Instant.now()); |
136 | 138 | } |
137 | | - |
138 | | - private static String getBackupName(String prefix, BlobItem backup) { |
139 | | - return backup.getName().substring(prefix.length() + 1); |
140 | | - } |
141 | 139 | } |
0 commit comments