2323import ch .cyberduck .core .box .io .swagger .client .model .Files ;
2424import ch .cyberduck .core .box .io .swagger .client .model .FilescontentAttributes ;
2525import 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 ;
2628import ch .cyberduck .core .exception .BackgroundException ;
2729import ch .cyberduck .core .exception .NotfoundException ;
2830import ch .cyberduck .core .http .AbstractHttpWriteFeature ;
2931import ch .cyberduck .core .http .DefaultHttpResponseExceptionMappingService ;
3032import ch .cyberduck .core .http .DelayedHttpEntityCallable ;
33+ import ch .cyberduck .core .http .HttpRange ;
3134import ch .cyberduck .core .http .HttpResponseOutputStream ;
3235import ch .cyberduck .core .io .Checksum ;
3336import ch .cyberduck .core .io .ChecksumCompute ;
3942import org .apache .http .HttpHeaders ;
4043import org .apache .http .client .HttpResponseException ;
4144import org .apache .http .client .methods .HttpPost ;
45+ import org .apache .http .client .methods .HttpPut ;
4246import org .apache .http .entity .ContentType ;
4347import org .apache .http .entity .mime .MultipartEntityBuilder ;
4448import 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}
0 commit comments