Skip to content

Commit e564281

Browse files
committed
stream files
1 parent 3b08398 commit e564281

6 files changed

Lines changed: 76 additions & 48 deletions

File tree

modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,16 +422,18 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
422422
{{#supportAsync}}
423423
{{^required}}
424424
if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
425-
let file_bytes = tokio::fs::read(param_value).await?;
425+
let file = TokioFile::open(param_value).await?;
426+
let stream = FramedRead::new(file, BytesCodec::new());
426427
let file_name = param_value.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
427-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
428+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name);
428429
multipart_form = multipart_form.part("{{{baseName}}}", file_part);
429430
}
430431
{{/required}}
431432
{{#required}}
432-
let file_bytes = tokio::fs::read(&{{{vendorExtensions.x-rust-param-identifier}}}).await?;
433+
let file = TokioFile::open(&{{{vendorExtensions.x-rust-param-identifier}}}).await?;
434+
let stream = FramedRead::new(file, BytesCodec::new());
433435
let file_name = {{{vendorExtensions.x-rust-param-identifier}}}.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
434-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
436+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name);
435437
multipart_form = multipart_form.part("{{{baseName}}}", file_part);
436438
{{/required}}
437439
{{/supportAsync}}

samples/client/others/rust/reqwest/multipart-async/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ serde = { version = "^1.0", features = ["derive"] }
1212
serde_json = "^1.0"
1313
serde_repr = "^0.1"
1414
url = "^2.5"
15-
tokio = { version = "^1.46.0", features = ["fs"] }
15+
tokio = { version = "^1.46.0", features = ["fs", "macros", "rt-multi-thread"] }
1616
tokio-util = { version = "^0.7", features = ["codec"] }
1717
reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "stream"] }
1818

1919
[dev-dependencies]
20-
tokio = { version = "^1.46.0", features = ["macros", "rt-multi-thread", "test-util"] }
20+
tokio = { version = "^1.46.0", features = ["macros", "rt-multi-thread"] }
2121

2222
[features]
2323
default = ["native-tls"]

samples/client/others/rust/reqwest/multipart-async/src/apis/default_api.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,16 @@ pub async fn upload_multiple_fields(configuration: &configuration::Configuration
8888
if let Some(param_value) = params.tags {
8989
multipart_form = multipart_form.text("tags", serde_json::to_string(&param_value)?);
9090
}
91-
let file_bytes = tokio::fs::read(&params.primary_file).await?;
91+
let file = TokioFile::open(&params.primary_file).await?;
92+
let stream = FramedRead::new(file, BytesCodec::new());
9293
let file_name = params.primary_file.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
93-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
94+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name);
9495
multipart_form = multipart_form.part("primaryFile", file_part);
9596
if let Some(ref param_value) = params.thumbnail {
96-
let file_bytes = tokio::fs::read(param_value).await?;
97+
let file = TokioFile::open(param_value).await?;
98+
let stream = FramedRead::new(file, BytesCodec::new());
9799
let file_name = param_value.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
98-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
100+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name);
99101
multipart_form = multipart_form.part("thumbnail", file_part);
100102
}
101103
req_builder = req_builder.multipart(multipart_form);
@@ -139,9 +141,10 @@ pub async fn upload_optional_file(configuration: &configuration::Configuration,
139141
multipart_form = multipart_form.text("metadata", param_value.to_string());
140142
}
141143
if let Some(ref param_value) = params.file {
142-
let file_bytes = tokio::fs::read(param_value).await?;
144+
let file = TokioFile::open(param_value).await?;
145+
let stream = FramedRead::new(file, BytesCodec::new());
143146
let file_name = param_value.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
144-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
147+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name);
145148
multipart_form = multipart_form.part("file", file_part);
146149
}
147150
req_builder = req_builder.multipart(multipart_form);
@@ -182,9 +185,10 @@ pub async fn upload_single_file(configuration: &configuration::Configuration, pa
182185
}
183186
let mut multipart_form = reqwest::multipart::Form::new();
184187
multipart_form = multipart_form.text("description", params.description.to_string());
185-
let file_bytes = tokio::fs::read(&params.file).await?;
188+
let file = TokioFile::open(&params.file).await?;
189+
let stream = FramedRead::new(file, BytesCodec::new());
186190
let file_name = params.file.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
187-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
191+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name);
188192
multipart_form = multipart_form.part("file", file_part);
189193
req_builder = req_builder.multipart(multipart_form);
190194

samples/client/others/rust/reqwest/multipart-async/tests/multipart_test.rs

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,56 @@
1-
/// This test verifies that async multipart file uploads use tokio::fs::read
2-
/// and reqwest::multipart::Part::bytes instead of the deprecated Form.file() method.
1+
/// This test verifies that async multipart file uploads use streaming
2+
/// (TokioFile + FramedRead + Part::stream) instead of buffering the entire file.
3+
///
4+
/// This approach:
5+
/// - Streams files instead of loading into memory (important for large files)
6+
/// - Uses the same pattern as body file uploads
7+
/// - Avoids the deprecated Form.file() method
38
///
49
/// Regression test for: https://github.com/OpenAPITools/openapi-generator/issues/XXXXX
510
611
#[tokio::test]
7-
async fn test_multipart_file_reading() {
12+
async fn test_multipart_file_streaming() {
13+
use tokio::fs::File as TokioFile;
14+
use tokio_util::codec::{BytesCodec, FramedRead};
15+
816
// Create a temporary file
917
let temp_dir = std::env::temp_dir();
1018
let test_file = temp_dir.join("test_upload.txt");
1119
let test_content = b"Hello, multipart upload test!";
1220

1321
std::fs::write(&test_file, test_content).expect("Failed to create test file");
1422

15-
// Verify tokio can read the file (what the generated code does)
16-
let file_bytes = tokio::fs::read(&test_file)
23+
// Verify the streaming pattern works (what the generated code does)
24+
let file = TokioFile::open(&test_file)
1725
.await
18-
.expect("Failed to read file with tokio::fs");
19-
assert_eq!(file_bytes, test_content);
20-
21-
// Verify we can create a Part::bytes (what the generated code does)
26+
.expect("Failed to open file with TokioFile");
27+
let stream = FramedRead::new(file, BytesCodec::new());
2228
let file_name = test_file
2329
.file_name()
2430
.map(|n| n.to_string_lossy().to_string())
2531
.unwrap_or_default();
2632
assert_eq!(file_name, "test_upload.txt");
2733

28-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
34+
// Create Part with streaming body
35+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream))
36+
.file_name(file_name);
2937

30-
// Verify we can create a form with the part
38+
// Verify we can create a form with the streaming part
3139
let form = reqwest::multipart::Form::new().part("file", file_part);
3240

33-
// If we got here, the API calls work correctly
34-
assert!(true, "Multipart form created successfully with Part::bytes");
41+
// If we got here, the streaming API calls work correctly
42+
assert!(true, "Multipart form created successfully with streaming");
3543

3644
// Cleanup
3745
std::fs::remove_file(test_file).ok();
3846
}
3947

40-
/// Test that optional file parameters work correctly
48+
/// Test that optional file parameters work correctly with streaming
4149
#[tokio::test]
4250
async fn test_optional_file_parameter() {
51+
use tokio::fs::File as TokioFile;
52+
use tokio_util::codec::{BytesCodec, FramedRead};
53+
4354
let temp_dir = std::env::temp_dir();
4455
let test_file = temp_dir.join("optional_test.txt");
4556
std::fs::write(&test_file, b"optional content").unwrap();
@@ -50,22 +61,27 @@ async fn test_optional_file_parameter() {
5061
let mut form = reqwest::multipart::Form::new();
5162

5263
if let Some(ref param_value) = file_param {
53-
let file_bytes = tokio::fs::read(param_value).await.unwrap();
64+
let file = TokioFile::open(param_value).await.unwrap();
65+
let stream = FramedRead::new(file, BytesCodec::new());
5466
let file_name = param_value
5567
.file_name()
5668
.map(|n| n.to_string_lossy().to_string())
5769
.unwrap_or_default();
58-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
70+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream))
71+
.file_name(file_name);
5972
form = form.part("file", file_part);
6073
}
6174

6275
// If we got here, optional file handling works
6376
std::fs::remove_file(test_file).ok();
6477
}
6578

66-
/// Test form with multiple fields (file + metadata)
79+
/// Test form with multiple fields (file + metadata) using streaming
6780
#[tokio::test]
6881
async fn test_multipart_with_metadata() {
82+
use tokio::fs::File as TokioFile;
83+
use tokio_util::codec::{BytesCodec, FramedRead};
84+
6985
let temp_dir = std::env::temp_dir();
7086
let test_file = temp_dir.join("with_metadata.txt");
7187
std::fs::write(&test_file, b"file with metadata").unwrap();
@@ -76,48 +92,57 @@ async fn test_multipart_with_metadata() {
7692
// Add text field
7793
form = form.text("description", "Test description");
7894

79-
// Add file field
80-
let file_bytes = tokio::fs::read(&test_file).await.unwrap();
95+
// Add file field with streaming
96+
let file = TokioFile::open(&test_file).await.unwrap();
97+
let stream = FramedRead::new(file, BytesCodec::new());
8198
let file_name = test_file
8299
.file_name()
83100
.map(|n| n.to_string_lossy().to_string())
84101
.unwrap_or_default();
85-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
102+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream))
103+
.file_name(file_name);
86104
form = form.part("file", file_part);
87105

88106
// Verify form was created successfully
89107
std::fs::remove_file(test_file).ok();
90108
}
91109

92-
/// Test multiple files in the same form
110+
/// Test multiple files in the same form with streaming
93111
#[tokio::test]
94112
async fn test_multiple_files() {
113+
use tokio::fs::File as TokioFile;
114+
use tokio_util::codec::{BytesCodec, FramedRead};
115+
95116
let temp_dir = std::env::temp_dir();
96117
let primary_file = temp_dir.join("primary.txt");
97118
let thumbnail_file = temp_dir.join("thumbnail.txt");
98119

99120
std::fs::write(&primary_file, b"primary content").unwrap();
100121
std::fs::write(&thumbnail_file, b"thumbnail content").unwrap();
101122

102-
// Build form with multiple files
123+
// Build form with multiple files using streaming
103124
let mut form = reqwest::multipart::Form::new();
104125

105-
// Add primary file (required)
106-
let file_bytes = tokio::fs::read(&primary_file).await.unwrap();
126+
// Add primary file (required) with streaming
127+
let file = TokioFile::open(&primary_file).await.unwrap();
128+
let stream = FramedRead::new(file, BytesCodec::new());
107129
let file_name = primary_file
108130
.file_name()
109131
.map(|n| n.to_string_lossy().to_string())
110132
.unwrap_or_default();
111-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
133+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream))
134+
.file_name(file_name);
112135
form = form.part("primaryFile", file_part);
113136

114-
// Add thumbnail file (optional)
115-
let file_bytes = tokio::fs::read(&thumbnail_file).await.unwrap();
137+
// Add thumbnail file (optional) with streaming
138+
let file = TokioFile::open(&thumbnail_file).await.unwrap();
139+
let stream = FramedRead::new(file, BytesCodec::new());
116140
let file_name = thumbnail_file
117141
.file_name()
118142
.map(|n| n.to_string_lossy().to_string())
119143
.unwrap_or_default();
120-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
144+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream))
145+
.file_name(file_name);
121146
form = form.part("thumbnail", file_part);
122147

123148
// Cleanup

samples/client/petstore/rust/reqwest/petstore-async/Cargo.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ tokio = { version = "^1.46.0", features = ["fs"] }
1717
tokio-util = { version = "^0.7", features = ["codec"] }
1818
reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "stream"] }
1919

20-
[dev-dependencies]
21-
wiremock = "0.6"
22-
tokio = { version = "^1.46.0", features = ["macros", "rt-multi-thread"] }
23-
2420
[features]
2521
default = ["native-tls"]
2622
native-tls = ["reqwest/native-tls"]

samples/client/petstore/rust/reqwest/petstore-async/src/apis/pet_api.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,9 +570,10 @@ pub async fn upload_file(configuration: &configuration::Configuration, params: U
570570
multipart_form = multipart_form.text("additionalMetadata", param_value.to_string());
571571
}
572572
if let Some(ref param_value) = params.file {
573-
let file_bytes = tokio::fs::read(param_value).await?;
573+
let file = TokioFile::open(param_value).await?;
574+
let stream = FramedRead::new(file, BytesCodec::new());
574575
let file_name = param_value.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
575-
let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(file_name);
576+
let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name);
576577
multipart_form = multipart_form.part("file", file_part);
577578
}
578579
req_builder = req_builder.multipart(multipart_form);

0 commit comments

Comments
 (0)