• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package software.amazon.awssdk.services.s3.transfer;
2 
3 /**
4  * The S3 Transfer Manager is a library that allows users to easily and
5  * optimally upload and downloads to and from S3.
6  * <p>
7  * The list of features includes:
8  * <ul>
9  *   <li>Parallel uploads and downloads</li>
10  *   <li>Bandwidth limiting</li>
11  *   <li>Pause and resume of transfers</li>
12  * </ul>
13  * <p>
14  * <b>Usage Example:</b>
15  * <pre>
16  * {@code
17  * // Create using all default configuration values
18  * S3TransferManager tm = S3TranferManager.create();
19  *
20  * // Using custom configuration values to set max upload speed to avoid
21  * // saturating the network interface.
22  * S3TransferManager tm = S3TransferManager.builder()
23  *             .maxUploadBytesSecond(32 * 1024 * 1024) // 32 MiB
24  *             .build()
25  *         .build();
26  * }
27  * </pre>
28  */
29 public interface S3TransferManager {
30     /**
31      * Download an object identified by the bucket and key from S3 to the given
32      * file.
33      * <p>
34      * <b>Usage Example:</b>
35      * <pre>
36      * {@code
37      * // Initiate transfer
38      * Download myFileDownload = tm.download(BUCKET, KEY, Paths.get("/tmp/myFile.txt");
39      * // Wait for transfer to complete
40      * myFileDownload().completionFuture().join();
41      * }
42      * </pre>
43      *
44      */
download(String bucket, String key, Path file)45     default Download download(String bucket, String key, Path file) {
46         return download(DownloadObjectRequest.builder()
47                         .downloadSpecification(DownloadObjectSpecification.fromApiRequest(GetObjectRequest.builder()
48                                 .bucket(bucket)
49                                 .key(key)
50                                 .build()))
51                         .build(),
52                 file);
53     }
54 
55     /**
56      * Download an object using an S3 presigned URL to the given file.
57      * <p>
58      * <b>Usage Example:</b>
59      * <pre>
60      * {@code
61      * // Initiate transfer
62      * Download myFileDownload = tm.download(myPresignedUrl, Paths.get("/tmp/myFile.txt");
63      * // Wait for transfer to complete
64      * myFileDownload()completionFuture().join();
65      * }
66      * </pre>
67      */
download(URL presignedUrl, Path file)68     default Download download(URL presignedUrl, Path file) {
69         return download(DownloadObjectRequest.builder()
70                         .downloadSpecification(DownloadObjectSpecification.fromPresignedUrl(presignedUrl))
71                         .build(),
72                 file);
73     }
74     /**
75      * Download an object in S3 to the given file.
76      *
77      * <p>
78      * <b>Usage Example:</b>
79      * <pre>
80      * {@code
81      * // Initiate the transfer
82      * Download myDownload = tm.download(DownloadObjectRequest.builder()
83      *         .downloadObjectSpecification(DownloadObjectSpecification.fromApiRequest(
84      *             GetObjectRequest.builder()
85      *                 .bucket(BUCKET)
86      *                 .key(KEY)
87      *                 .build()))
88      *          // Set the known length of the object to avoid a HeadObject call
89      *         .size(1024 * 1024 * 5)
90      *         .build(),
91      *         Paths.get("/tmp/myFile.txt"));
92      * // Wait for the transfer to complete
93      * myDownload.completionFuture().join();
94      * }
95      * </pre>
96      */
download(DownloadObjectRequest request, Path file)97     Download download(DownloadObjectRequest request, Path file);
98 
99     /**
100      * Resume a previously paused object download.
101      */
resumeDownloadObject(DownloadObjectState downloadObjectState)102     Download resumeDownloadObject(DownloadObjectState downloadObjectState);
103 
104     /**
105      * Download the set of objects from the bucket with the given prefix to a directory.
106      * <p>
107      * The transfer manager will use '/' as the path delimiter.
108      * <p>
109      * <b>Usage Example:</b>
110      * <pre>
111      * {@code
112      * DownloadDirectory myDownload = tm.downloadDirectory(myBucket, myPrefix, Paths.get("/tmp");
113      * myDowload.completionFuture().join();
114      * }
115      * </pre>
116      *
117      * @param bucket The bucket.
118      * @param prefix The prefix.
119      * @param destinationDirectory The directory where the objects will be
120      * downloaded to.
121      */
downloadDirectory(String bucket, String prefix, Path destinationDirectory)122     DownloadDirectory downloadDirectory(String bucket, String prefix, Path destinationDirectory);
123 
124     /**
125      * Upload a directory of files to the given S3 bucket and under the given
126      * prefix.
127      * <p>
128      * <b>Usage Example:</b>
129      * <pre>
130      * {@code
131      * UploadDirectory myUpload = tm.uploadDirectory(myBucket, myPrefix, Paths.get("/path/to/my/directory));
132      * myUpload.completionFuture().join();
133      * }
134      * </pre>
135      */
uploadDirectory(String bucket, String prefix, Path direcrtory)136     UploadDirectory uploadDirectory(String bucket, String prefix, Path direcrtory);
137 
138     /**
139      * Upload a file to S3.
140      * <p>
141      * <b>Usage Example:</b>
142      * <pre>
143      * {@code
144      * UploadObject myUpload = tm.uploadObject(myBucket, myKey, Paths.get("myFile.txt"));
145      * myUpload.completionFuture().join();
146      * }
147      * </pre>
148      */
upload(String bucket, String key, Path file)149     default Upload upload(String bucket, String key, Path file) {
150         return upload(UploadObjectRequest.builder()
151                         .uploadSpecification(UploadObjectSpecification.fromApiRequest(PutObjectRequest.builder()
152                                 .bucket(bucket)
153                                 .key(key)
154                                 .build()))
155                         .build(),
156                 file);
157     }
158 
159     /**
160      * Upload a file to S3 using the given presigned URL.
161      * <p>
162      * <b>Usage Example:</b>
163      * <pre>
164      * {@code
165      * Upload myUpload = tm.upload(myPresignedUrl, Paths.get("myFile.txt"));
166      * myUpload.completionFuture().join();
167      * }
168      * </pre>
169      */
upload(URL presignedUrl, Path file)170     default Upload upload(URL presignedUrl, Path file) {
171         return upload(UploadObjectRequest.builder()
172                         .uploadSpecification(UploadObjectSpecification.fromPresignedUrl(presignedUrl))
173                         .build(),
174                 file);
175     }
176 
177     /**
178      * Upload a file to S3.
179      */
upload(UploadObjectRequest request, Path file)180     Upload upload(UploadObjectRequest request, Path file);
181 
182     /**
183      * Resume a previously paused object upload.
184      */
resumeUploadObject(UploadObjectState uploadObjectState)185     Upload resumeUploadObject(UploadObjectState uploadObjectState);
186 
187     /**
188      * Create an {@code S3TransferManager} using the default values.
189      */
create()190     static S3TransferManager create() {
191         return builder().build();
192     }
193 
builder()194     static S3TransferManager.Builder builder() {
195         return ...;
196     }
197 
198     interface Builder {
199         /**
200          * The custom S3AsyncClient this transfer manager will use to make calls
201          * to S3.
202          */
s3client(S3AsyncClient s3Client)203         Builder s3client(S3AsyncClient s3Client);
204 
205         /**
206          * The max number of requests the Transfer Manager will have at any
207          * point in time. This must be less than or equal to the max concurrent
208          * setting on the S3 client.
209          */
maxConcurrency(Integer maxConcurrency)210         Builder maxConcurrency(Integer maxConcurrency);
211 
212         /**
213          * The aggregate max upload rate in bytes per second over all active
214          * upload transfers. The default is unlimited.
215          */
maxUploadBytesPerSecond(Long maxUploadBytesPerSecond)216         Builder maxUploadBytesPerSecond(Long maxUploadBytesPerSecond);
217 
218         /**
219          * The aggregate max download rate in bytes per second over all active
220          * download transfers. The default value is unlimited.
221          */
maxDownloadBytesPerSecond(Long maxDownloadBytesPerSecond)222         Builder maxDownloadBytesPerSecond(Long maxDownloadBytesPerSecond);
223 
224         /**
225          * Add a progress listener to the currently configured list of
226          * listeners.
227          */
addProgressListener(TransferProgressListener progressListener)228         Builder addProgressListener(TransferProgressListener progressListener);
229 
230         /**
231          * Set the list of progress listeners, overwriting any currently
232          * configured list.
233          */
progressListeners(Collection<? extends TransferProgressListener> progressListeners)234         Builder progressListeners(Collection<? extends TransferProgressListener> progressListeners);
235 
build()236         S3TransferManager build();
237     }
238 }
239 
240 /**
241  * Configuration object for multipart downloads.
242  */
243 public interface MultipartDownloadConfiguration {
244     /**
245      * Whether multipart downloads are enabled.
246      */
enableMultipartDownloads()247     public Boolean enableMultipartDownloads();
248 
249     /**
250      * The minimum size for an object to be downloaded in multiple parts.
251      */
multipartDownloadThreshold()252     public Long multipartDownloadThreshold();
253 
254     /**
255      * The maximum number of parts objects are to be downloaded in.
256      */
maxDownloadPartCount()257     public Integer maxDownloadPartCount();
258 
259     /**
260      * The minimum size for each part.
261      */
minDownloadPartSize()262     public Long minDownloadPartSize();
263 }
264 
265 /**
266  * Configuration object for multipart uploads.
267  */
268 public final class MultipartUploadConfiguration {
269     /**
270      * Whether multipart uploads should be enabled.
271      */
enableMultipartUploads()272     public Boolean enableMultipartUploads();
273 
274     /**
275      * The minimum size for an object to be uploaded in multipe parts.
276      */
multipartUploadThreshold()277     public Long multipartUploadThreshold();
278 
279     /**
280      * The maximum number of perts to upload an object in.
281      */
maxUploadPartCount()282     public Integer maxUploadPartCount();
283 
284     /**
285      * The minimum size for each uploaded part.
286      */
minUploadPartSize()287     public Long minUploadPartSize();
288 }
289 
290 /**
291  * Override configuration for a single transfer.
292  */
293 public interface TransferOverrideConfiguration {
294     /**
295      * The maximum rate for this transfer in bytes per second.
296      */
maxTransferBytesPerSecond()297     Long maxTransferBytesPerSecond();
298 
299     /**
300      * Override configuration for multipart downloads.
301      */
multipartDownloadConfiguration()302     MultipartDownloadConfiguration multipartDownloadConfiguration();
303 
304     /**
305      * Override configuration for multipart uploads.
306      */
multipartUploadConfiguration()307     MultipartUploadConfiguration multipartUploadConfiguration();
308 }
309 
310 
311 /**
312  * A factory capable of creating the streams for individual parts of a given
313  * object to be uploaded to S3.
314  * <p>
315  * There is no ordering guaranatee for when {@link
316  * #streamForPart(PartUploadContext)} is called.
317  */
318 public interface TransferRequestBody {
319     /**
320      * Return the stream for the object part described by given {@link
321      * PartUploadContext}.
322      *
323      * @param context The context describing the part to be uploaded.
324      * @return The part stream.
325      */
requestBodyForPart(PartUploadContext context)326     Publisher<ByteBuffer> requestBodyForPart(PartUploadContext context);
327 
328     /**
329      * Return the stream for a entire object to be uploaded as a single part.
330      */
requestBodyForObject(SinglePartUploadContext context)331     Publisher<ByteBuffer> requestBodyForObject(SinglePartUploadContext context);
332 
333     /**
334      * Create a factory that creates streams for individual parts of the given
335      * file.
336      *
337      * @param file The file whose parts the factory will create streams for.
338      * @return The stream factory.
339      */
forFile(Path file)340     static ObjectPartStreamCreator forFile(Path file) {
341         return ...;
342     }
343 }
344 
345 /**
346  * A factory capable of creating the {@link AsyncResponseTransformer} to handle
347  * each downloaded object part.
348  * <p>
349  * There is no ordering guarantee for when {@link
350  * #transformerForPart(PartDownloadContext)} invocations. It is invoked when the
351  * response from S3 is received for the given part.
352  */
353 public interface TransferResponseTransformer {
354     /**
355      * Return a transformer for downloading a single part of an object.
356      */
transformerForPart(MultipartDownloadContext context)357     AsyncResponseTransformer<GetObjectResponse, ?> transformerForPart(MultipartDownloadContext context);
358 
359     /**
360      * Return a transformer for downloading an entire object as a single part.
361      */
362     AsyncResponseTransformer<GetObjectResponse, ?> transformerForObject(SinglePartDownloadContext context)
363 
364     /**
365      * Return a factory capable of creating transformers that will recombine the
366      * object parts to a single file on disk.
367      */
368     static TransferResponseTransformer forFile(Path file) {
369         return ...;
370     }
371 }
372 
373 /**
374  * The context object for the upload of an object part to S3.
375  */
376 public interface MultipartUploadContext {
377     /**
378      * The original upload request given to the transfer manager.
379      */
uploadRequest()380     UploadObjectRequest uploadRequest();
381 
382     /**
383      * The request sent to S3 to initiate the multipart upload.
384      */
createMultipartRequest()385     CreateMultipartUploadRequest createMultipartRequest();
386 
387     /**
388      * The upload request to be sent to S3 for this part.
389      */
uploadPartRequest()390     UploadPartRequest uploadPartRequest();
391 
392     /**
393      * The offset from the beginning of the object where this part begins.
394      */
partOffset()395     long partOffset();
396 }
397 
398 public interface SinglePartUploadContext {
399     /**
400      * The original upload request given to the transfer manager.
401      */
uploadRequest()402     UploadObjectRequest uploadRequest();
403 
404     /**
405      * The request to be sent to S3 to upload the object as a single part.
406      */
objectRequest()407     PutObjectRequest objectRequest();
408 }
409 
410 /**
411  * Context object for an individual object part for a multipart download.
412  */
413 public interface MultipartDownloadContext {
414     /**
415      * The original download request given to the Transfer Manager.
416      */
downloadRequest()417     DownloadObjectRequest downloadRequest();
418 
419     /**
420      * The part number.
421      */
partNumber()422     int partNumber();
423 
424     /**
425      * The offset from the beginning of the object where this part begins.
426      */
partOffset()427     long partOffset();
428 
429     /**
430      * The size of the part requested in bytes.
431      */
size()432     long size();
433 
434     /**
435      * Whether this is the last part of the object.
436      */
isLastPart()437     boolean isLastPart();
438 }
439 
440 /**
441  * Context object for a single part download of an object.
442  */
443 public interface SinglePartDownloadContext {
444     /**
445      * The original download request given to the Transfer Manager.
446      */
downloadRequest()447     DownloadObjectRequest downloadRequest();
448 
449     /**
450      * The request sent to S3 for this object. This is empty if downloading a presigned URL.
451      */
objectRequest()452     GetObjectRequest objectRequest();
453 }
454 
455 /**
456  * Progress listener for a Transfer.
457  * <p>
458  * The SDK guarantees that calls to {@link #transferProgressEvent(EventContext)}
459  * are externally synchronized.
460  */
461 public interface TransferProgressListener {
462     /**
463      * Called when a new progress event is available for a Transfer.
464      *
465      * @param ctx The context object for the given transfer event.
466      */
transferProgressEvent(EventContext ctx)467     void transferProgressEvent(EventContext ctx);
468 
469     interface EventContext {
470         /**
471          * The transfer this listener associated with.
472          */
transfer()473         Transfer transfer();
474     }
475 
476     interface Initiated extends EventContext {
477         /**
478          * The amount of time that has elapsed since the transfer was
479          * initiated.
480          */
elapsedTime()481         Duration elapsedTime();
482     }
483 
484     interface BytesTransferred extends Initiated {
485         /**
486          * The transfer request for the object whose bytes were transferred.
487          */
objectRequest()488         TransferObjectRequest objectRequest();
489 
490         /**
491          * The number of bytes transferred for this event.
492          */
bytes()493         long bytes();
494 
495         /**
496          * The total size of the object.
497          */
size()498         long size();
499 
500         /**
501          * If the transfer of the given object is complete.
502          */
complete()503         boolean complete();
504     }
505 
506     interface Completed extends Initiated {
507     }
508 
509     interface Cancelled extends Initiated {
510     }
511 
512     interface Failed extends Initiated {
513         /**
514          * The error.
515          */
error()516         Throwable error();
517     }
518 }
519 
520 
521 /**
522  * A download transfer of a single object from S3.
523  */
524 public interface Download extends Transfer {
525     @Override
pause()526     DownloadObjectState pause();
527 }
528 
529 /**
530  * An upload transfer of a single object to S3.
531  */
532 public interface Upload extends Transfer {
533     @Override
pause()534     UploadObjectState pause();
535 }
536 
537 /**
538  * The state of an object download.
539  */
540 public interface DownloadState extends TransferState {
541     /**
542      * Persist this state so it can later be resumed.
543      */
persistTo(OutputStream os)544     void persistTo(OutputStream os);
545 
546     /**
547      * Load a persisted transfer which can then be resumed.
548      */
loadFrom(Inputstream is)549     static DownloadState loadFrom(Inputstream is) {
550         ...
551     }
552 }
553 
554 /**
555  * The state of an object upload.
556  */
557 public interface UploadState extends TransferState {
558     /**
559      * Persist this state so it can later be resumed.
560      */
persistTo(OutputStream os)561     void persistTo(OutputStream os);
562 
563     /**
564      * Load a persisted transfer which can then be resumed.
565      */
loadFrom(Inputstream is)566     static UploadState loadFrom(Inputstream is) {
567         ...
568     }
569 }
570 
571 /**
572  * Represents the transfer of one or more objects to or from S3.
573  */
574 public interface Transfer {
575 
completionFuture()576     CompletableFuture<? extends CompletedTransfer> completionFuture();
577 
578     /**
579      * Pause this transfer, cancelling any requests in progress.
580      * <p>
581      * The returned state object can be used to resume this transfer at a later
582      * time.
583      *
584      * @throws IllegalStateException If this transfer is completed or cancelled.
585      * @throws UnsupportedOperationException If the transfer does not support
586      * pause and resume.
587      */
pause()588     TransferState pause();
589 }
590 
591 public interface CompletedTransfer {
592     /**
593      * The metrics for this transfer.
594      */
metrics()595     TransferMetrics metrics();
596 }
597 
598 /**
599  * Metrics for a completed transfer.
600  */
601 public interface TransferMetrics {
602     /**
603      * The number of milliseconds that elapsed before this transfer completed.
604      */
elapsedMillis()605     long elapsedMillis();
606 
607     /**
608      * The total number of bytes transferred.
609      */
bytesTransferred()610     long bytesTransferred();
611 }
612 
613 
614 /**
615  * A request to download an object. The object to download is specified using
616  * the {@link DownloadObjectSpecification} union type.
617  */
618 public class DownloadObjectRequest extends AbstractTransferRequest {
619     /**
620      * The specification for how to download the object.
621      */
downloadSpecification()622     DownloadObjectSpecification downloadSpecification();
623 
624     /**
625      * The size of the object to be downloaded.
626      */
size()627     public Long size();
628 
forPresignedUrl(URL presignedUrl)629     public static DownloadObjectRequest forPresignedUrl(URL presignedUrl) {
630         ...
631     }
632 
forBucketAndKey(String bucket, String key)633     public static DownloadObjectRequest forBucketAndKey(String bucket, String key) {
634         ...
635     }
636 
637     public interface Builder extends AbstractTransferRequest.Builder {
638         /**
639          * The specification for how to download the object.
640          */
downloadSpecification(DownloadObjectSpecification downloadSpecification)641         Builder downloadSpecification(DownloadObjectSpecification downloadSpecification);
642 
643        /**
644          * Set the override configuration for this request.
645          */
646         @Override
overrideConfiguration(TransferOverrideConfiguration config)647         Builder overrideConfiguration(TransferOverrideConfiguration config);
648 
649         /**
650          * Set the progress listeners for this request.
651          */
652         @Override
progressListeners(Collection<TransferProgressListener> progressListeners)653         Builder progressListeners(Collection<TransferProgressListener> progressListeners);
654 
655         /**
656          * Add an additional progress listener for this request, appending it to
657          * the list of currently configured listeners on this request.
658          */
659         @Override
addProgressListener(TransferProgressListener progressListener)660         Builder addProgressListener(TransferProgressListener progressListener);
661 
662         /**
663          * Set the optional size of the object to be downloaded.
664          */
size(Long size)665         Builder size(Long size);
666 
build()667         DownloadObjectRequest build();
668     }
669 }
670 
671 /**
672  * Union type to contain the different ways to express an object download from
673  * S3.
674  */
675 public class DownloadObjectSpecification {
676     /**
677      * Return this specification as a presigned URL.
678      *
679      * @throws IllegalStateException If this specifier is not a presigned URL.
680      */
asPresignedUrl()681     URL asPresignedUrl();
682 
683     /**
684      * Return this specification as a {@link GetObjectRequest API request}.
685      *
686      * @throws IllegalStateException If this specifier is not an API request.
687      */
asApiRequest()688     GetObjectRequest asApiRequest();
689 
690     /**
691      * Returns {@code true} if this is a presigned URL, {@code false} otherwise.
692      */
isPresignedUrl()693     boolean isPresignedUrl();
694 
695     /**
696      * Returns {@code true} if this is an API request, {@code false} otherwise.
697      */
isApiRequest()698     boolean isApiRequest();
699 
700     /**
701      * Create an instance from a presigned URL.
702      */
fromPresignedUrl(URL presignedUrl)703     static DownloadObjectSpecification fromPresignedUrl(URL presignedUrl) {
704         ...
705     }
706 
707     /**
708      * Create an instance from an API request.
709      */
fromApiRequest(GetObjectRequest apiRequest)710     static DownloadObjectSpecification fromApiRequest(GetObjectRequest apiRequest) {
711         ...
712     }
713 }
714 
715 /**
716  * A request to upload an object. The object to upload is specified using the
717  * {@link UploadObjectSpecification} union type.
718  */
719 public class UploadObjectRequest extends AbstractTransferRequest {
720     /**
721      * The specification for how to upload the object.
722      */
uploadSpecification()723     UploadbjectSpecification uploadSpecification();
724 
725     /**
726      * The size of the object to be uploaded.
727      */
size()728     long size();
729 
forPresignedUrl(URL presignedUrl)730     public static UploadObjectRequest forPresignedUrl(URL presignedUrl) {
731         ...
732     }
733 
forBucketAndKey(String bucket, String key)734     public static UploadObjectRequest forBucketAndKey(String bucket, String key) {
735         ...
736     }
737 
738     public interface Builder extends AbstractTransferRequest.Builder {
739         /**
740          * The specification for how to upload the object.
741          */
uploadSpecification(UploadObjectSpecification uploadSpecification)742         Builder uploadSpecification(UploadObjectSpecification uploadSpecification);
743 
744        /**
745          * Set the override configuration for this request.
746          */
747         @Override
overrideConfiguration(TransferOverrideConfiguration config)748         Builder overrideConfiguration(TransferOverrideConfiguration config);
749 
750         /**
751          * Set the progress listeners for this request.
752          */
753         @Override
progressListeners(Collection<TransferProgressListener> progressListeners)754         Builder progressListeners(Collection<TransferProgressListener> progressListeners);
755 
756         /**
757          * Add an additional progress listener for this request, appending it to
758          * the list of currently configured listeners on this request.
759          */
760         @Override
addProgressListener(TransferProgressListener progressListener)761         Builder addProgressListener(TransferProgressListener progressListener);
762 
build()763         UploadObjectRequest build();
764     }
765 }
766 
767 /**
768  * Union type to contain the different ways to express an object upload from
769  * S3.
770  */
771 public class UploadObjectSpecification {
772     /**
773      * Return this specification as a presigned URL.
774      *
775      * @throws IllegalStateException If this specifier is not a presigned URL.
776      */
asPresignedUrl()777     URL asPresignedUrl();
778 
779     /**
780      * Return this specification as a {@link PutObjectRequest API request}.
781      *
782      * @throws IllegalStateException If this specifier is not an API request.
783      */
asApiRequest()784     PutObjectRequest asApiRequest();
785 
786     /**
787      * Returns {@code true} if this is a presigned URL, {@code false} otherwise.
788      */
isPresignedUrl()789     boolean isPresignedUrl();
790 
791     /**
792      * Returns {@code true} if this is an API request, {@code false} otherwise.
793      */
isApiRequest()794     boolean isApiRequest();
795 
796     /**
797      * Create an instance from a presigned URL.
798      */
fromPresignedUrl(URL presignedUrl)799     static UploadObjectSpecification fromPresignedUrl(URL presignedUrl) {
800         ...
801     }
802 
803     /**
804      * Create an instance from an API request.
805      */
fromApiRequest(PutObjectRequest apiRequest)806     static UploadObjectSpecification fromApiRequest(PutObjectRequest apiRequest) {
807         ...
808     }
809 }
810