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