Skip to content

Commit 628af94

Browse files
committed
Merge implementations to handle both chunked and full multipart upload.
1 parent 54eaacd commit 628af94

File tree

3 files changed

+138
-180
lines changed

3 files changed

+138
-180
lines changed

box/src/main/java/ch/cyberduck/core/box/BoxChunkedWriteFeature.java

Lines changed: 0 additions & 111 deletions
This file was deleted.

box/src/main/java/ch/cyberduck/core/box/BoxWriteFeature.java

Lines changed: 137 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
import ch.cyberduck.core.box.io.swagger.client.model.Files;
2424
import ch.cyberduck.core.box.io.swagger.client.model.FilescontentAttributes;
2525
import ch.cyberduck.core.box.io.swagger.client.model.FilescontentAttributesParent;
26+
import ch.cyberduck.core.box.io.swagger.client.model.UploadPart;
27+
import ch.cyberduck.core.box.io.swagger.client.model.UploadedPart;
2628
import ch.cyberduck.core.exception.BackgroundException;
2729
import ch.cyberduck.core.exception.NotfoundException;
2830
import ch.cyberduck.core.http.AbstractHttpWriteFeature;
2931
import ch.cyberduck.core.http.DefaultHttpResponseExceptionMappingService;
3032
import ch.cyberduck.core.http.DelayedHttpEntityCallable;
33+
import ch.cyberduck.core.http.HttpRange;
3134
import ch.cyberduck.core.http.HttpResponseOutputStream;
3235
import ch.cyberduck.core.io.Checksum;
3336
import ch.cyberduck.core.io.ChecksumCompute;
@@ -39,6 +42,7 @@
3942
import org.apache.http.HttpHeaders;
4043
import org.apache.http.client.HttpResponseException;
4144
import org.apache.http.client.methods.HttpPost;
45+
import org.apache.http.client.methods.HttpPut;
4246
import org.apache.http.entity.ContentType;
4347
import org.apache.http.entity.mime.MultipartEntityBuilder;
4448
import org.apache.http.message.BasicHeader;
@@ -67,74 +71,13 @@ public BoxWriteFeature(final BoxSession session, final BoxFileidProvider fileid)
6771

6872
@Override
6973
public HttpResponseOutputStream<File> write(final Path file, final TransferStatus status, final ConnectionCallback callback) throws BackgroundException {
70-
final DelayedHttpEntityCallable<File> command = new DelayedHttpEntityCallable<File>(file) {
71-
@Override
72-
public File call(final HttpEntity entity) throws BackgroundException {
73-
try {
74-
final HttpPost request;
75-
if(status.isExists()) {
76-
request = new HttpPost(String.format("%s/files/%s/content?fields=%s", client.getBasePath(),
77-
fileid.getFileId(file),
78-
String.join(",", BoxAttributesFinderFeature.DEFAULT_FIELDS)));
79-
}
80-
else {
81-
request = new HttpPost(String.format("%s/files/content?fields=%s", client.getBasePath(),
82-
String.join(",", BoxAttributesFinderFeature.DEFAULT_FIELDS)));
83-
}
84-
final Checksum checksum = status.getChecksum();
85-
if(Checksum.NONE != checksum) {
86-
switch(checksum.algorithm) {
87-
case sha1:
88-
request.addHeader(HttpHeaders.CONTENT_MD5, checksum.hash);
89-
}
90-
}
91-
final ByteArrayOutputStream content = new ByteArrayOutputStream();
92-
new JSON().getContext(null).writeValue(content, new FilescontentAttributes()
93-
.name(file.getName())
94-
.parent(new FilescontentAttributesParent().id(fileid.getFileId(file.getParent())))
95-
.contentCreatedAt(status.getCreated() != null ? new DateTime(status.getCreated()) : null)
96-
.contentModifiedAt(status.getModified() != null ? new DateTime(status.getModified()) : null)
97-
);
98-
final MultipartEntityBuilder multipart = MultipartEntityBuilder.create();
99-
multipart.addBinaryBody("attributes", content.toByteArray());
100-
final ByteArrayOutputStream out = new ByteArrayOutputStream();
101-
entity.writeTo(out);
102-
multipart.addBinaryBody("file", out.toByteArray(),
103-
null == status.getMime() ? ContentType.APPLICATION_OCTET_STREAM : ContentType.create(status.getMime()), file.getName());
104-
request.setEntity(multipart.build());
105-
if(status.isExists()) {
106-
if(StringUtils.isNotBlank(status.getRemote().getETag())) {
107-
request.addHeader(new BasicHeader(HttpHeaders.IF_MATCH, status.getRemote().getETag()));
108-
}
109-
else {
110-
log.warn("Missing remote attributes in transfer status to read current ETag for {}", file);
111-
}
112-
}
113-
final Files files = session.getClient().execute(request, new BoxClientErrorResponseHandler<Files>() {
114-
@Override
115-
public Files handleEntity(final HttpEntity entity) throws IOException {
116-
return new JSON().getContext(null).readValue(entity.getContent(), Files.class);
117-
}
118-
});
119-
log.debug("Received response {} for upload of {}", files, file);
120-
if(files.getEntries().stream().findFirst().isPresent()) {
121-
return files.getEntries().stream().findFirst().get();
122-
}
123-
throw new NotfoundException(file.getAbsolute());
124-
}
125-
catch(HttpResponseException e) {
126-
throw new DefaultHttpResponseExceptionMappingService().map("Upload {0} failed", e, file);
127-
}
128-
catch(IOException e) {
129-
throw new DefaultIOExceptionMappingService().map("Upload {0} failed", e, file);
130-
}
131-
}
132-
133-
@Override
134-
public long getContentLength() {
135-
return -1L;
136-
}
137-
};
74+
final DelayedHttpEntityCallable<File> command;
75+
if(status.isSegment()) {
76+
command = new ChunkDelayedHttpEntityCallable(file, status);
77+
}
78+
else {
79+
command = new MultipartDelayedHttpEntityCallable(file, status);
80+
}
13881
return this.write(file, status, command);
13982
}
14083

@@ -147,4 +90,130 @@ public ChecksumCompute checksum(final Path file, final TransferStatus status) {
14790
public EnumSet<Flags> features(final Path file) {
14891
return EnumSet.of(Flags.timestamp, Flags.checksum, Flags.mime);
14992
}
93+
94+
private class MultipartDelayedHttpEntityCallable extends DelayedHttpEntityCallable<File> {
95+
private final Path file;
96+
private final TransferStatus status;
97+
98+
public MultipartDelayedHttpEntityCallable(final Path file, final TransferStatus status) {
99+
super(file);
100+
this.file = file;
101+
this.status = status;
102+
}
103+
104+
@Override
105+
public File call(final HttpEntity entity) throws BackgroundException {
106+
try {
107+
final HttpPost request;
108+
if(status.isExists()) {
109+
request = new HttpPost(String.format("%s/files/%s/content?fields=%s", client.getBasePath(),
110+
fileid.getFileId(file),
111+
String.join(",", BoxAttributesFinderFeature.DEFAULT_FIELDS)));
112+
}
113+
else {
114+
request = new HttpPost(String.format("%s/files/content?fields=%s", client.getBasePath(),
115+
String.join(",", BoxAttributesFinderFeature.DEFAULT_FIELDS)));
116+
}
117+
final Checksum checksum = status.getChecksum();
118+
if(Checksum.NONE != checksum) {
119+
switch(checksum.algorithm) {
120+
case sha1:
121+
request.addHeader(HttpHeaders.CONTENT_MD5, checksum.hash);
122+
}
123+
}
124+
final ByteArrayOutputStream content = new ByteArrayOutputStream();
125+
new JSON().getContext(null).writeValue(content, new FilescontentAttributes()
126+
.name(file.getName())
127+
.parent(new FilescontentAttributesParent().id(fileid.getFileId(file.getParent())))
128+
.contentCreatedAt(status.getCreated() != null ? new DateTime(status.getCreated()) : null)
129+
.contentModifiedAt(status.getModified() != null ? new DateTime(status.getModified()) : null)
130+
);
131+
final MultipartEntityBuilder multipart = MultipartEntityBuilder.create();
132+
multipart.addBinaryBody("attributes", content.toByteArray());
133+
final ByteArrayOutputStream out = new ByteArrayOutputStream();
134+
entity.writeTo(out);
135+
multipart.addBinaryBody("file", out.toByteArray(),
136+
null == status.getMime() ? ContentType.APPLICATION_OCTET_STREAM : ContentType.create(status.getMime()), file.getName());
137+
request.setEntity(multipart.build());
138+
if(status.isExists()) {
139+
if(StringUtils.isNotBlank(status.getRemote().getETag())) {
140+
request.addHeader(new BasicHeader(HttpHeaders.IF_MATCH, status.getRemote().getETag()));
141+
}
142+
else {
143+
log.warn("Missing remote attributes in transfer status to read current ETag for {}", file);
144+
}
145+
}
146+
final Files files = session.getClient().execute(request, new BoxClientErrorResponseHandler<Files>() {
147+
@Override
148+
public Files handleEntity(final HttpEntity entity) throws IOException {
149+
return new JSON().getContext(null).readValue(entity.getContent(), Files.class);
150+
}
151+
});
152+
log.debug("Received response {} for upload of {}", files, file);
153+
if(files.getEntries().stream().findFirst().isPresent()) {
154+
return files.getEntries().stream().findFirst().get();
155+
}
156+
throw new NotfoundException(file.getAbsolute());
157+
}
158+
catch(HttpResponseException e) {
159+
throw new DefaultHttpResponseExceptionMappingService().map("Upload {0} failed", e, file);
160+
}
161+
catch(IOException e) {
162+
throw new DefaultIOExceptionMappingService().map("Upload {0} failed", e, file);
163+
}
164+
}
165+
166+
@Override
167+
public long getContentLength() {
168+
return -1L;
169+
}
170+
}
171+
172+
private class ChunkDelayedHttpEntityCallable extends DelayedHttpEntityCallable<File> {
173+
private final Path file;
174+
private final TransferStatus status;
175+
176+
public ChunkDelayedHttpEntityCallable(final Path file, final TransferStatus status) {
177+
super(file);
178+
this.file = file;
179+
this.status = status;
180+
}
181+
182+
@Override
183+
public File call(final HttpEntity entity) throws BackgroundException {
184+
try {
185+
final HttpRange range = HttpRange.withStatus(new TransferStatus()
186+
.setLength(status.getLength())
187+
.setOffset(status.getOffset()));
188+
final String uploadSessionId = status.getParameters().get(BoxLargeUploadService.UPLOAD_SESSION_ID);
189+
final String overall_length = status.getParameters().get(BoxLargeUploadService.OVERALL_LENGTH);
190+
log.debug("Send range {} for file {}", range, file);
191+
final HttpPut request = new HttpPut(String.format("%s/files/upload_sessions/%s", client.getBasePath(), uploadSessionId));
192+
// Must not overlap with the range of a part already uploaded this session.
193+
request.addHeader(new BasicHeader(HttpHeaders.CONTENT_RANGE, String.format("bytes %d-%d/%d", range.getStart(), range.getEnd(),
194+
Long.valueOf(overall_length))));
195+
request.addHeader(new BasicHeader("Digest", String.format("sha=%s", status.getChecksum().base64)));
196+
request.setEntity(entity);
197+
final UploadPart response = session.getClient().execute(request, new BoxClientErrorResponseHandler<UploadedPart>() {
198+
@Override
199+
public UploadedPart handleEntity(final HttpEntity entity1) throws IOException {
200+
return new JSON().getContext(null).readValue(entity1.getContent(), UploadedPart.class);
201+
}
202+
}).getPart();
203+
log.debug("Received response {} for upload of {}", response, file);
204+
return new File().size(response.getSize()).sha1(response.getSha1()).id(response.getPartId());
205+
}
206+
catch(HttpResponseException e) {
207+
throw new DefaultHttpResponseExceptionMappingService().map("Upload {0} failed", e, file);
208+
}
209+
catch(IOException e) {
210+
throw new DefaultIOExceptionMappingService().map("Upload {0} failed", e, file);
211+
}
212+
}
213+
214+
@Override
215+
public long getContentLength() {
216+
return -1L;
217+
}
218+
}
150219
}

box/src/test/java/ch/cyberduck/core/box/BoxLargeUploadServiceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void testUploadLargeFileInChunks() throws Exception {
5858
status.setChecksum(new SHA1ChecksumCompute().compute(local.getInputStream(), new TransferStatus()));
5959
status.setLength(content.length);
6060
final BytecountStreamListener count = new BytecountStreamListener();
61-
final File response = s.upload(new BoxChunkedWriteFeature(session, fileid), file, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), new DisabledProgressListener(), count, status, new DisabledConnectionCallback());
61+
final File response = s.upload(new BoxWriteFeature(session, fileid), file, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), new DisabledProgressListener(), count, status, new DisabledConnectionCallback());
6262
assertTrue(status.isComplete());
6363
assertNotNull(response.getSha1());
6464
assertEquals(content.length, count.getSent());

0 commit comments

Comments
 (0)