1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.transfer.s3.model; 17 18 import java.io.ByteArrayInputStream; 19 import java.io.IOException; 20 import java.io.InputStream; 21 import java.io.OutputStream; 22 import java.nio.charset.StandardCharsets; 23 import java.nio.file.Files; 24 import java.nio.file.Path; 25 import java.time.Instant; 26 import java.util.Objects; 27 import java.util.Optional; 28 import java.util.OptionalLong; 29 import java.util.function.Consumer; 30 import software.amazon.awssdk.annotations.SdkPublicApi; 31 import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; 32 import software.amazon.awssdk.core.SdkBytes; 33 import software.amazon.awssdk.core.exception.SdkClientException; 34 import software.amazon.awssdk.services.s3.model.GetObjectRequest; 35 import software.amazon.awssdk.transfer.s3.S3TransferManager; 36 import software.amazon.awssdk.transfer.s3.config.TransferRequestOverrideConfiguration; 37 import software.amazon.awssdk.transfer.s3.internal.serialization.ResumableFileDownloadSerializer; 38 import software.amazon.awssdk.utils.IoUtils; 39 import software.amazon.awssdk.utils.ToString; 40 import software.amazon.awssdk.utils.Validate; 41 import software.amazon.awssdk.utils.builder.CopyableBuilder; 42 import software.amazon.awssdk.utils.builder.ToCopyableBuilder; 43 44 /** 45 * An opaque token that holds the state and can be used to resume a paused download operation. 46 * <p> 47 * <b>Serialization: </b>When serializing this token, the following structures will not be preserved/persisted: 48 * <ul> 49 * <li>{@link TransferRequestOverrideConfiguration}</li> 50 * <li>{@link AwsRequestOverrideConfiguration} (from {@link GetObjectRequest})</li> 51 * </ul> 52 * 53 * @see S3TransferManager#downloadFile(DownloadFileRequest) 54 */ 55 @SdkPublicApi 56 public final class ResumableFileDownload implements ResumableTransfer, 57 ToCopyableBuilder<ResumableFileDownload.Builder, ResumableFileDownload> { 58 59 private final DownloadFileRequest downloadFileRequest; 60 private final long bytesTransferred; 61 private final Instant s3ObjectLastModified; 62 private final Long totalSizeInBytes; 63 private final Instant fileLastModified; 64 ResumableFileDownload(DefaultBuilder builder)65 private ResumableFileDownload(DefaultBuilder builder) { 66 this.downloadFileRequest = Validate.paramNotNull(builder.downloadFileRequest, "downloadFileRequest"); 67 this.bytesTransferred = builder.bytesTransferred == null ? 0 : Validate.isNotNegative(builder.bytesTransferred, 68 "bytesTransferred"); 69 this.s3ObjectLastModified = builder.s3ObjectLastModified; 70 this.totalSizeInBytes = Validate.isPositiveOrNull(builder.totalSizeInBytes, "totalSizeInBytes"); 71 this.fileLastModified = builder.fileLastModified; 72 } 73 74 @Override equals(Object o)75 public boolean equals(Object o) { 76 if (this == o) { 77 return true; 78 } 79 if (o == null || getClass() != o.getClass()) { 80 return false; 81 } 82 83 ResumableFileDownload that = (ResumableFileDownload) o; 84 85 if (bytesTransferred != that.bytesTransferred) { 86 return false; 87 } 88 if (!downloadFileRequest.equals(that.downloadFileRequest)) { 89 return false; 90 } 91 if (!Objects.equals(s3ObjectLastModified, that.s3ObjectLastModified)) { 92 return false; 93 } 94 if (!Objects.equals(fileLastModified, that.fileLastModified)) { 95 return false; 96 } 97 return Objects.equals(totalSizeInBytes, that.totalSizeInBytes); 98 } 99 100 @Override hashCode()101 public int hashCode() { 102 int result = downloadFileRequest.hashCode(); 103 result = 31 * result + (int) (bytesTransferred ^ (bytesTransferred >>> 32)); 104 result = 31 * result + (s3ObjectLastModified != null ? s3ObjectLastModified.hashCode() : 0); 105 result = 31 * result + (fileLastModified != null ? fileLastModified.hashCode() : 0); 106 result = 31 * result + (totalSizeInBytes != null ? totalSizeInBytes.hashCode() : 0); 107 return result; 108 } 109 builder()110 public static Builder builder() { 111 return new DefaultBuilder(); 112 } 113 114 /** 115 * @return the {@link DownloadFileRequest} to resume 116 */ downloadFileRequest()117 public DownloadFileRequest downloadFileRequest() { 118 return downloadFileRequest; 119 } 120 121 /** 122 * Retrieve the number of bytes that have been transferred. 123 * @return the number of bytes 124 */ bytesTransferred()125 public long bytesTransferred() { 126 return bytesTransferred; 127 } 128 129 /** 130 * Last modified time of the S3 object since last pause, or {@link Optional#empty()} if unknown 131 */ s3ObjectLastModified()132 public Optional<Instant> s3ObjectLastModified() { 133 return Optional.ofNullable(s3ObjectLastModified); 134 } 135 136 /** 137 * Last modified time of the file since last pause 138 */ fileLastModified()139 public Instant fileLastModified() { 140 return fileLastModified; 141 } 142 143 /** 144 * The total size of the transfer in bytes or {@link OptionalLong#empty()} if unknown 145 * 146 * @return the optional total size of the transfer. 147 */ totalSizeInBytes()148 public OptionalLong totalSizeInBytes() { 149 return totalSizeInBytes == null ? OptionalLong.empty() : OptionalLong.of(totalSizeInBytes); 150 } 151 152 @Override toString()153 public String toString() { 154 return ToString.builder("ResumableFileDownload") 155 .add("bytesTransferred", bytesTransferred) 156 .add("fileLastModified", fileLastModified) 157 .add("s3ObjectLastModified", s3ObjectLastModified) 158 .add("totalSizeInBytes", totalSizeInBytes) 159 .add("downloadFileRequest", downloadFileRequest) 160 .build(); 161 } 162 163 /** 164 * Persists this download object to a file in Base64-encoded JSON format. 165 * 166 * @param path The path to the file to which you want to write the serialized download object. 167 */ 168 @Override serializeToFile(Path path)169 public void serializeToFile(Path path) { 170 try { 171 Files.write(path, ResumableFileDownloadSerializer.toJson(this)); 172 } catch (IOException e) { 173 throw SdkClientException.create("Failed to write to " + path, e); 174 } 175 } 176 177 /** 178 * Writes the serialized JSON data representing this object to an output stream. 179 * Note that the {@link OutputStream} is not closed or flushed after writing. 180 * 181 * @param outputStream The output stream to write the serialized object to. 182 */ 183 @Override serializeToOutputStream(OutputStream outputStream)184 public void serializeToOutputStream(OutputStream outputStream) { 185 byte[] bytes = ResumableFileDownloadSerializer.toJson(this); 186 try { 187 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); 188 IoUtils.copy(byteArrayInputStream, outputStream); 189 } catch (IOException e) { 190 throw SdkClientException.create("Failed to write this download object to the given OutputStream", e); 191 } 192 } 193 194 /** 195 * Returns the serialized JSON data representing this object as a string. 196 */ 197 @Override serializeToString()198 public String serializeToString() { 199 return new String(ResumableFileDownloadSerializer.toJson(this), StandardCharsets.UTF_8); 200 } 201 202 /** 203 * Returns the serialized JSON data representing this object as an {@link SdkBytes} object. 204 * 205 * @return the serialized JSON as {@link SdkBytes} 206 */ 207 @Override serializeToBytes()208 public SdkBytes serializeToBytes() { 209 return SdkBytes.fromByteArrayUnsafe(ResumableFileDownloadSerializer.toJson(this)); 210 } 211 212 /** 213 * Returns the serialized JSON data representing this object as an {@link InputStream}. 214 * 215 * @return the serialized JSON input stream 216 */ 217 @Override serializeToInputStream()218 public InputStream serializeToInputStream() { 219 return new ByteArrayInputStream(ResumableFileDownloadSerializer.toJson(this)); 220 } 221 222 /** 223 * Deserialize data at the given path into a {@link ResumableFileDownload}. 224 * 225 * @param path The {@link Path} to the file with serialized data 226 * @return the deserialized {@link ResumableFileDownload} 227 */ fromFile(Path path)228 public static ResumableFileDownload fromFile(Path path) { 229 try (InputStream stream = Files.newInputStream(path)) { 230 return ResumableFileDownloadSerializer.fromJson(stream); 231 } catch (IOException e) { 232 throw SdkClientException.create("Failed to create a ResumableFileDownload from " + path, e); 233 } 234 } 235 236 /** 237 * Deserialize bytes with JSON data into a {@link ResumableFileDownload}. 238 * 239 * @param bytes the serialized data 240 * @return the deserialized {@link ResumableFileDownload} 241 */ fromBytes(SdkBytes bytes)242 public static ResumableFileDownload fromBytes(SdkBytes bytes) { 243 return ResumableFileDownloadSerializer.fromJson(bytes.asByteArrayUnsafe()); 244 } 245 246 /** 247 * Deserialize a string with JSON data into a {@link ResumableFileDownload}. 248 * 249 * @param contents the serialized data 250 * @return the deserialized {@link ResumableFileDownload} 251 */ fromString(String contents)252 public static ResumableFileDownload fromString(String contents) { 253 return ResumableFileDownloadSerializer.fromJson(contents); 254 } 255 256 @Override toBuilder()257 public Builder toBuilder() { 258 return new DefaultBuilder(this); 259 } 260 261 public interface Builder extends CopyableBuilder<Builder, ResumableFileDownload> { 262 263 /** 264 * Sets the download file request 265 * 266 * @param downloadFileRequest the download file request 267 * @return a reference to this object so that method calls can be chained together. 268 */ downloadFileRequest(DownloadFileRequest downloadFileRequest)269 Builder downloadFileRequest(DownloadFileRequest downloadFileRequest); 270 271 /** 272 * The {@link DownloadFileRequest} request 273 * 274 * <p> 275 * This is a convenience method that creates an instance of the {@link DownloadFileRequest} builder avoiding the 276 * need to create one manually via {@link DownloadFileRequest#builder()}. 277 * 278 * @param downloadFileRequestBuilder the download file request builder 279 * @return a reference to this object so that method calls can be chained together. 280 * @see #downloadFileRequest(DownloadFileRequest) 281 */ downloadFileRequest(Consumer<DownloadFileRequest.Builder> downloadFileRequestBuilder)282 default ResumableFileDownload.Builder downloadFileRequest(Consumer<DownloadFileRequest.Builder> 283 downloadFileRequestBuilder) { 284 DownloadFileRequest request = DownloadFileRequest.builder() 285 .applyMutation(downloadFileRequestBuilder) 286 .build(); 287 downloadFileRequest(request); 288 return this; 289 } 290 291 /** 292 * Sets the number of bytes transferred 293 * 294 * @param bytesTransferred the number of bytes transferred 295 * @return a reference to this object so that method calls can be chained together. 296 */ bytesTransferred(Long bytesTransferred)297 Builder bytesTransferred(Long bytesTransferred); 298 299 /** 300 * Sets the total transfer size in bytes 301 * @param totalSizeInBytes the transfer size in bytes 302 * @return a reference to this object so that method calls can be chained together. 303 */ totalSizeInBytes(Long totalSizeInBytes)304 Builder totalSizeInBytes(Long totalSizeInBytes); 305 306 /** 307 * Sets the last modified time of the object 308 * 309 * @param s3ObjectLastModified the last modified time of the object 310 * @return a reference to this object so that method calls can be chained together. 311 */ s3ObjectLastModified(Instant s3ObjectLastModified)312 Builder s3ObjectLastModified(Instant s3ObjectLastModified); 313 314 /** 315 * Sets the last modified time of the object 316 * 317 * @param lastModified the last modified time of the object 318 * @return a reference to this object so that method calls can be chained together. 319 */ fileLastModified(Instant lastModified)320 Builder fileLastModified(Instant lastModified); 321 } 322 323 private static final class DefaultBuilder implements Builder { 324 325 private DownloadFileRequest downloadFileRequest; 326 private Long bytesTransferred; 327 private Instant s3ObjectLastModified; 328 private Long totalSizeInBytes; 329 private Instant fileLastModified; 330 DefaultBuilder()331 private DefaultBuilder() { 332 } 333 DefaultBuilder(ResumableFileDownload persistableFileDownload)334 private DefaultBuilder(ResumableFileDownload persistableFileDownload) { 335 this.downloadFileRequest = persistableFileDownload.downloadFileRequest; 336 this.bytesTransferred = persistableFileDownload.bytesTransferred; 337 this.totalSizeInBytes = persistableFileDownload.totalSizeInBytes; 338 this.fileLastModified = persistableFileDownload.fileLastModified; 339 this.s3ObjectLastModified = persistableFileDownload.s3ObjectLastModified; 340 } 341 342 @Override downloadFileRequest(DownloadFileRequest downloadFileRequest)343 public Builder downloadFileRequest(DownloadFileRequest downloadFileRequest) { 344 this.downloadFileRequest = downloadFileRequest; 345 return this; 346 } 347 348 @Override bytesTransferred(Long bytesTransferred)349 public Builder bytesTransferred(Long bytesTransferred) { 350 this.bytesTransferred = bytesTransferred; 351 return this; 352 } 353 354 @Override totalSizeInBytes(Long totalSizeInBytes)355 public Builder totalSizeInBytes(Long totalSizeInBytes) { 356 this.totalSizeInBytes = totalSizeInBytes; 357 return this; 358 } 359 360 @Override s3ObjectLastModified(Instant s3ObjectLastModified)361 public Builder s3ObjectLastModified(Instant s3ObjectLastModified) { 362 this.s3ObjectLastModified = s3ObjectLastModified; 363 return this; 364 } 365 366 @Override fileLastModified(Instant fileLastModified)367 public Builder fileLastModified(Instant fileLastModified) { 368 this.fileLastModified = fileLastModified; 369 return this; 370 } 371 372 @Override build()373 public ResumableFileDownload build() { 374 return new ResumableFileDownload(this); 375 } 376 } 377 } 378