1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.commons.compress.archivers.zip;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.nio.channels.FileChannel;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.nio.file.StandardCopyOption;
28 import java.nio.file.StandardOpenOption;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.TreeMap;
34
35 import org.apache.commons.io.file.PathUtils;
36
37
38
39
40
41
42 final class ZipSplitOutputStream extends RandomAccessOutputStream {
43
44
45
46
47
48
49
50
51
52
53 private static final long ZIP_SEGMENT_MIN_SIZE = 64 * 1024L;
54 private static final long ZIP_SEGMENT_MAX_SIZE = 4294967295L;
55
56 private FileChannel currentChannel;
57 private FileRandomAccessOutputStream outputStream;
58 private Path zipFile;
59 private final long splitSize;
60 private long totalPosition;
61 private int currentSplitSegmentIndex;
62 private long currentSplitSegmentBytesWritten;
63 private boolean finished;
64 private final byte[] singleByte = new byte[1];
65 private final List<Long> diskToPosition = new ArrayList<>();
66 private final TreeMap<Long, Path> positionToFiles = new TreeMap<>();
67
68
69
70
71
72
73
74
75
76
77 ZipSplitOutputStream(final File zipFile, final long splitSize) throws IllegalArgumentException, IOException {
78 this(zipFile.toPath(), splitSize);
79 }
80
81
82
83
84
85
86
87
88
89
90
91 ZipSplitOutputStream(final Path zipFile, final long splitSize) throws IllegalArgumentException, IOException {
92 if (splitSize < ZIP_SEGMENT_MIN_SIZE || splitSize > ZIP_SEGMENT_MAX_SIZE) {
93 throw new IllegalArgumentException("Zip split segment size should between 64K and 4,294,967,295");
94 }
95 this.zipFile = zipFile;
96 this.splitSize = splitSize;
97 this.outputStream = new FileRandomAccessOutputStream(zipFile);
98 this.currentChannel = this.outputStream.channel();
99 this.positionToFiles.put(0L, this.zipFile);
100 this.diskToPosition.add(0L);
101
102 writeZipSplitSignature();
103 }
104
105 public long calculateDiskPosition(final long disk, final long localOffset) throws IOException {
106 if (disk >= Integer.MAX_VALUE) {
107 throw new IOException("Disk number exceeded internal limits: limit=" + Integer.MAX_VALUE + " requested=" + disk);
108 }
109 return diskToPosition.get((int) disk) + localOffset;
110 }
111
112 @Override
113 public void close() throws IOException {
114 if (!finished) {
115 finish();
116 }
117 }
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137 private Path createNewSplitSegmentFile(final Integer zipSplitSegmentSuffixIndex) throws IOException {
138 final Path newFile = getSplitSegmentFileName(zipSplitSegmentSuffixIndex);
139
140 if (Files.exists(newFile)) {
141 throw new IOException("split ZIP segment " + newFile + " already exists");
142 }
143 return newFile;
144 }
145
146
147
148
149
150
151 private void finish() throws IOException {
152 if (finished) {
153 throw new IOException("This archive has already been finished");
154 }
155 final Path path = zipFile;
156
157 final String zipFileBaseName = PathUtils.getBaseName(path);
158 outputStream.close();
159 Files.move(zipFile, zipFile.resolveSibling(zipFileBaseName + ".zip"), StandardCopyOption.ATOMIC_MOVE);
160 finished = true;
161 }
162
163 public long getCurrentSplitSegmentBytesWritten() {
164 return currentSplitSegmentBytesWritten;
165 }
166
167 public int getCurrentSplitSegmentIndex() {
168 return currentSplitSegmentIndex;
169 }
170
171 private Path getSplitSegmentFileName(final Integer zipSplitSegmentSuffixIndex) {
172 final int newZipSplitSegmentSuffixIndex = zipSplitSegmentSuffixIndex == null ? currentSplitSegmentIndex + 2 : zipSplitSegmentSuffixIndex;
173 final Path path = zipFile;
174 final String baseName = PathUtils.getBaseName(path);
175 final StringBuilder extension = new StringBuilder(".z");
176 if (newZipSplitSegmentSuffixIndex <= 9) {
177 extension.append("0").append(newZipSplitSegmentSuffixIndex);
178 } else {
179 extension.append(newZipSplitSegmentSuffixIndex);
180 }
181
182 final Path parent = zipFile.getParent();
183 final String dir = Objects.nonNull(parent) ? parent.toAbsolutePath().toString() : ".";
184 return zipFile.getFileSystem().getPath(dir, baseName + extension.toString());
185 }
186
187
188
189
190
191
192 private void openNewSplitSegment() throws IOException {
193 Path newFile;
194 if (currentSplitSegmentIndex == 0) {
195 outputStream.close();
196 newFile = createNewSplitSegmentFile(1);
197 Files.move(zipFile, newFile, StandardCopyOption.ATOMIC_MOVE);
198 this.positionToFiles.put(0L, newFile);
199 }
200
201 newFile = createNewSplitSegmentFile(null);
202
203 outputStream.close();
204 outputStream = new FileRandomAccessOutputStream(newFile);
205 currentChannel = outputStream.channel();
206 currentSplitSegmentBytesWritten = 0;
207 zipFile = newFile;
208 currentSplitSegmentIndex++;
209 this.diskToPosition.add(this.totalPosition);
210 this.positionToFiles.put(this.totalPosition, newFile);
211 }
212
213 @Override
214 public long position() {
215 return totalPosition;
216 }
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232 public void prepareToWriteUnsplittableContent(final long unsplittableContentSize) throws IllegalArgumentException, IOException {
233 if (unsplittableContentSize > this.splitSize) {
234 throw new IllegalArgumentException("The unsplittable content size is bigger than the split segment size");
235 }
236
237 final long bytesRemainingInThisSegment = this.splitSize - this.currentSplitSegmentBytesWritten;
238 if (bytesRemainingInThisSegment < unsplittableContentSize) {
239 openNewSplitSegment();
240 }
241 }
242
243 @Override
244 public void write(final byte[] b) throws IOException {
245 write(b, 0, b.length);
246 }
247
248
249
250
251
252
253
254
255
256 @Override
257 public void write(final byte[] b, final int off, final int len) throws IOException {
258 if (len <= 0) {
259 return;
260 }
261
262 if (currentSplitSegmentBytesWritten >= splitSize) {
263 openNewSplitSegment();
264 write(b, off, len);
265 } else if (currentSplitSegmentBytesWritten + len > splitSize) {
266 final int bytesToWriteForThisSegment = (int) splitSize - (int) currentSplitSegmentBytesWritten;
267 write(b, off, bytesToWriteForThisSegment);
268 openNewSplitSegment();
269 write(b, off + bytesToWriteForThisSegment, len - bytesToWriteForThisSegment);
270 } else {
271 outputStream.write(b, off, len);
272 currentSplitSegmentBytesWritten += len;
273 totalPosition += len;
274 }
275 }
276
277 @Override
278 public void write(final int i) throws IOException {
279 singleByte[0] = (byte) (i & 0xff);
280 write(singleByte);
281 }
282
283 @Override
284 public void writeAll(final byte[] b, final int off, final int len, final long atPosition) throws IOException {
285 long remainingPosition = atPosition;
286 for (int remainingOff = off, remainingLen = len; remainingLen > 0; ) {
287 final Map.Entry<Long, Path> segment = positionToFiles.floorEntry(remainingPosition);
288 final Long segmentEnd = positionToFiles.higherKey(remainingPosition);
289 if (segmentEnd == null) {
290 ZipIoUtil.writeAll(this.currentChannel, ByteBuffer.wrap(b, remainingOff, remainingLen), remainingPosition - segment.getKey());
291 remainingPosition += remainingLen;
292 remainingOff += remainingLen;
293 remainingLen = 0;
294 } else if (remainingPosition + remainingLen <= segmentEnd) {
295 writeToSegment(segment.getValue(), remainingPosition - segment.getKey(), b, remainingOff, remainingLen);
296 remainingPosition += remainingLen;
297 remainingOff += remainingLen;
298 remainingLen = 0;
299 } else {
300 final int toWrite = Math.toIntExact(segmentEnd - remainingPosition);
301 writeToSegment(segment.getValue(), remainingPosition - segment.getKey(), b, remainingOff, toWrite);
302 remainingPosition += toWrite;
303 remainingOff += toWrite;
304 remainingLen -= toWrite;
305 }
306 }
307 }
308
309 private void writeToSegment(final Path segment, final long position, final byte[] b, final int off, final int len) throws IOException {
310 try (FileChannel channel = FileChannel.open(segment, StandardOpenOption.WRITE)) {
311 ZipIoUtil.writeAll(channel, ByteBuffer.wrap(b, off, len), position);
312 }
313 }
314
315
316
317
318
319
320 private void writeZipSplitSignature() throws IOException {
321 outputStream.write(ZipArchiveOutputStream.DD_SIG);
322 currentSplitSegmentBytesWritten += ZipArchiveOutputStream.DD_SIG.length;
323 totalPosition += ZipArchiveOutputStream.DD_SIG.length;
324 }
325 }