Skip to content

Commit 94b9d60

Browse files
committed
initial file based download
update interfaces
1 parent 6578816 commit 94b9d60

19 files changed

+4627
-6
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"services": [
3+
{
4+
"serviceName": "S3",
5+
"type": "minor",
6+
"changeLogMessages": [
7+
"Create DownloadWithResponseAsync API and implement multi part download"
8+
]
9+
}
10+
]
11+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*******************************************************************************
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use
4+
* this file except in compliance with the License. A copy of the License is located at
5+
*
6+
* http://aws.amazon.com/apache2.0
7+
*
8+
* or in the "license" file accompanying this file.
9+
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10+
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
* *****************************************************************************
13+
* __ _ _ ___
14+
* ( )( \/\/ )/ __)
15+
* /__\ \ / \__ \
16+
* (_)(_) \/\/ (___/
17+
*
18+
* AWS SDK for .NET
19+
* API Version: 2006-03-01
20+
*
21+
*/
22+
using System;
23+
using System.IO;
24+
using System.Security.Cryptography;
25+
26+
namespace Amazon.S3.Transfer.Internal
27+
{
28+
/// <summary>
29+
/// Handles atomic file operations for multipart downloads using SEP-compliant temporary file pattern.
30+
/// Creates .s3tmp.{uniqueId} files and ensures atomic commits to prevent partial file corruption.
31+
/// </summary>
32+
internal class AtomicFileHandler : IDisposable
33+
{
34+
private string _tempFilePath;
35+
private readonly object _lockObject = new object();
36+
private bool _disposed = false;
37+
38+
/// <summary>
39+
/// Creates a temporary file with unique identifier for atomic operations.
40+
/// Pattern: {destinationPath}.s3tmp.{8-char-unique-id}
41+
/// </summary>
42+
public string CreateTemporaryFile(string destinationPath)
43+
{
44+
if (string.IsNullOrEmpty(destinationPath))
45+
throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
46+
47+
// Generate unique 8-character identifier per SEP requirement
48+
var uniqueId = GenerateUniqueId(8);
49+
var tempPath = $"{destinationPath}.s3tmp.{uniqueId}";
50+
51+
// Ensure uniqueness in directory (handle extremely rare collisions)
52+
int attempts = 0;
53+
while (File.Exists(tempPath) && attempts < 100)
54+
{
55+
uniqueId = GenerateUniqueId(8);
56+
tempPath = $"{destinationPath}.s3tmp.{uniqueId}";
57+
attempts++;
58+
}
59+
60+
if (attempts >= 100)
61+
throw new InvalidOperationException("Unable to generate unique temporary file name");
62+
63+
// Create directory if it doesn't exist (Directory.CreateDirectory is idempotent)
64+
var directory = Path.GetDirectoryName(tempPath);
65+
if (!string.IsNullOrEmpty(directory))
66+
{
67+
Directory.CreateDirectory(directory);
68+
}
69+
70+
// Create empty file to reserve the name
71+
using (var stream = File.Create(tempPath))
72+
{
73+
// File created - immediately close it
74+
}
75+
76+
lock (_lockObject)
77+
{
78+
_tempFilePath = tempPath;
79+
}
80+
81+
return tempPath;
82+
}
83+
84+
/// <summary>
85+
/// Atomically commits the temporary file to the final destination.
86+
/// Uses File.Replace for atomic replacement when destination exists, or File.Move for new files.
87+
/// This prevents data loss if the process crashes during commit.
88+
/// </summary>
89+
public void CommitFile(string tempPath, string destinationPath)
90+
{
91+
if (string.IsNullOrEmpty(tempPath))
92+
throw new ArgumentException("Temp path cannot be null or empty", nameof(tempPath));
93+
if (string.IsNullOrEmpty(destinationPath))
94+
throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
95+
96+
if (!File.Exists(tempPath))
97+
throw new FileNotFoundException($"Temporary file not found: {tempPath}");
98+
99+
try
100+
{
101+
// Use File.Replace for atomic replacement when overwriting existing file
102+
// This prevents data loss if process crashes between delete and move operations
103+
// File.Replace is atomic on Windows (ReplaceFile API) and Unix (rename syscall)
104+
if (File.Exists(destinationPath))
105+
{
106+
File.Replace(tempPath, destinationPath, null);
107+
}
108+
else
109+
{
110+
// For new files, File.Move is sufficient and atomic on same volume
111+
File.Move(tempPath, destinationPath);
112+
}
113+
114+
lock (_lockObject)
115+
{
116+
if (_tempFilePath == tempPath)
117+
_tempFilePath = null; // Successfully committed
118+
}
119+
}
120+
catch (Exception ex)
121+
{
122+
throw new InvalidOperationException($"Failed to commit temporary file {tempPath} to {destinationPath}", ex);
123+
}
124+
}
125+
126+
/// <summary>
127+
/// Cleans up temporary file in case of failure or cancellation.
128+
/// Safe to call multiple times.
129+
/// </summary>
130+
public void CleanupOnFailure(string tempPath = null)
131+
{
132+
var pathToClean = tempPath;
133+
134+
if (string.IsNullOrEmpty(pathToClean))
135+
{
136+
lock (_lockObject)
137+
{
138+
pathToClean = _tempFilePath;
139+
}
140+
}
141+
142+
if (string.IsNullOrEmpty(pathToClean))
143+
return;
144+
145+
try
146+
{
147+
if (File.Exists(pathToClean))
148+
{
149+
File.Delete(pathToClean);
150+
}
151+
152+
lock (_lockObject)
153+
{
154+
if (_tempFilePath == pathToClean)
155+
_tempFilePath = null;
156+
}
157+
}
158+
catch (IOException)
159+
{
160+
// Log warning but don't throw - cleanup is best effort
161+
// In production, this would use proper logging infrastructure
162+
}
163+
catch (UnauthorizedAccessException)
164+
{
165+
// Log warning but don't throw - cleanup is best effort
166+
}
167+
}
168+
169+
/// <summary>
170+
/// Generates a cryptographically secure unique identifier of specified length.
171+
/// Uses base32 encoding to avoid filesystem-problematic characters.
172+
/// </summary>
173+
private string GenerateUniqueId(int length)
174+
{
175+
const string base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; // RFC 4648 base32
176+
177+
using (var rng = RandomNumberGenerator.Create())
178+
{
179+
var bytes = new byte[length];
180+
rng.GetBytes(bytes);
181+
182+
var result = new char[length];
183+
for (int i = 0; i < length; i++)
184+
{
185+
result[i] = base32Chars[bytes[i] % base32Chars.Length];
186+
}
187+
188+
return new string(result);
189+
}
190+
}
191+
192+
public void Dispose()
193+
{
194+
if (!_disposed)
195+
{
196+
// Cleanup any remaining temp file
197+
CleanupOnFailure();
198+
_disposed = true;
199+
}
200+
}
201+
}
202+
}

sdk/src/Services/S3/Custom/Transfer/Internal/BufferedPartDataHandler.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ public BufferedPartDataHandler(
4444
_config = config ?? throw new ArgumentNullException(nameof(config));
4545
}
4646

47+
public Task PrepareAsync(DownloadDiscoveryResult discoveryResult, CancellationToken cancellationToken)
48+
{
49+
// No preparation needed for buffered handler - buffers are created on demand
50+
return Task.CompletedTask;
51+
}
52+
4753
public async Task ProcessPartAsync(
4854
int partNumber,
4955
GetObjectResponse response,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*******************************************************************************
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use
4+
* this file except in compliance with the License. A copy of the License is located at
5+
*
6+
* http://aws.amazon.com/apache2.0
7+
*
8+
* or in the "license" file accompanying this file.
9+
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10+
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
* *****************************************************************************
13+
* __ _ _ ___
14+
* ( )( \/\/ )/ __)
15+
* /__\ \ / \__ \
16+
* (_)(_) \/\/ (___/
17+
*
18+
* AWS SDK for .NET
19+
* API Version: 2006-03-01
20+
*
21+
*/
22+
using System;
23+
24+
namespace Amazon.S3.Transfer.Internal
25+
{
26+
/// <summary>
27+
/// Configuration settings for file-based multipart downloads.
28+
/// Extends base coordinator settings with file-specific parameters.
29+
/// </summary>
30+
internal class FileDownloadConfiguration : DownloadCoordinatorConfiguration
31+
{
32+
/// <summary>
33+
/// Buffer size for file I/O operations.
34+
/// </summary>
35+
public int BufferSize { get; set; }
36+
37+
/// <summary>
38+
/// Destination file path for the download.
39+
/// </summary>
40+
public string DestinationFilePath { get; set; }
41+
42+
/// <summary>
43+
/// Creates a FileDownloadConfiguration with the specified configuration values.
44+
/// </summary>
45+
/// <param name="concurrentServiceRequests">Maximum concurrent HTTP requests for downloading parts.</param>
46+
/// <param name="bufferSize">Buffer size used for file I/O operations.</param>
47+
/// <param name="targetPartSizeBytes">Target size for each part in bytes.</param>
48+
/// <param name="destinationFilePath">Destination file path for the download.</param>
49+
/// <exception cref="ArgumentOutOfRangeException">Thrown when any numeric parameter is less than or equal to 0.</exception>
50+
/// <exception cref="ArgumentException">Thrown when destinationFilePath is null or empty.</exception>
51+
public FileDownloadConfiguration(
52+
int concurrentServiceRequests,
53+
int bufferSize,
54+
long targetPartSizeBytes,
55+
string destinationFilePath)
56+
: base(concurrentServiceRequests, targetPartSizeBytes)
57+
{
58+
if (bufferSize <= 0)
59+
throw new ArgumentOutOfRangeException(nameof(bufferSize), "Must be greater than 0");
60+
if (string.IsNullOrWhiteSpace(destinationFilePath))
61+
throw new ArgumentException("Destination file path cannot be null or empty", nameof(destinationFilePath));
62+
63+
BufferSize = bufferSize;
64+
DestinationFilePath = destinationFilePath;
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)