Skip to content

Commit 6b86ba7

Browse files
dblockclaude
andcommitted
Add support for multiple files in files_upload_v2 method
- Add files parameter to accept array of file objects - Support both single file and multiple files upload - Add proper validation for file objects in array - Add comprehensive tests for multiple files scenarios - Maintain backward compatibility with existing single file usage Fixes #566 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fb8f307 commit 6b86ba7

File tree

6 files changed

+189
-22
lines changed

6 files changed

+189
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* [#559](https://github.com/slack-ruby/slack-ruby-client/pull/559): Enable name-to-id translation of non-public channels - [@eizengan](https://github.com/eizengan).
55
* [#560](https://github.com/slack-ruby/slack-ruby-client/pull/560): Name-to-id translation can supply all sensible list options - [@eizengan](https://github.com/eizengan).
66
* [#561](https://github.com/slack-ruby/slack-ruby-client/issues/563): Raise InvalidSignature when verifying a request without a signature - [@wesleyjellis](https://github.com/wesleyjellis).
7+
* [#567](https://github.com/slack-ruby/slack-ruby-client/pull/567): Add support for multiple files in `files_upload_v2` method - [@dblock](https://github.com/dblock).
78
* Your contribution here.
89

910
### 2.6.0 (2025/05/24)

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ Upload files with [sequenced API calls](https://api.slack.com/messaging/files#up
173173

174174
This library provides a helper method `files_upload_v2` that wraps the three separate API calls.
175175

176+
Upload a single file.
177+
176178
```ruby
177179
client.files_upload_v2(
178180
# required options
@@ -188,6 +190,19 @@ client.files_upload_v2(
188190
)
189191
```
190192

193+
Upload multiple files.
194+
195+
```ruby
196+
client.files_upload_v2(
197+
files: [
198+
{ filename: 'report.pdf', content: File.read('/users/me/report.pdf'), title: 'Monthly Report' },
199+
{ filename: 'data.csv', content: File.read('/users/me/data.csv'), title: 'Raw Data' }
200+
],
201+
channels: ['#general'],
202+
initial_comment: 'Here are the monthly results!'
203+
)
204+
```
205+
191206
You can use a channel ID passed as `channel_id`, a single channel as `channel`, an array of channel IDs as `channels`, or a channel name or names (prefixed with `#`) in `files_upload_v2`. Lookup by name is not supported by the Slack API and this method called invokes `conversations_list` in order to locate the channel ID. This invocation can have a cost if you have many Slack channels and is only recommended when you intend to list channels anyway.
192207

193208
Note: This library includes a `files_upload` method that uses a deprecated endpoint `files.upload` that will [no longer be supported on 3/11/2025](https://api.slack.com/methods/files.upload#markdown).

examples/files_upload_v2/files_upload_v2.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@
3535
content: SecureRandom.hex,
3636
channel_id: channel_id
3737
).files.first.permalink_public
38+
39+
client.files_upload_v2(
40+
files: [
41+
{ filename: 'files_upload_v2_to_general_first_file.txt', content: SecureRandom.hex },
42+
{ filename: 'files_upload_v2_to_general_second_file.txt', content: SecureRandom.hex }
43+
],
44+
channels: ['#general'],
45+
).files.each { |file| puts file.permalink_public }

lib/slack/web/api/helpers/files.rb

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ module Files
1313
# Name of the file being uploaded.
1414
# @option params [string] :content
1515
# File contents via a POST variable.
16+
# @option params [Array<Hash>] :files
17+
# Array of file objects with :filename, :content, and optionally :title, :alt_txt, :snippet_type.
1618
# @option params [string] :alt_txt
1719
# Description of image for screen-reader.
1820
# @option params [string] :snippet_type
@@ -32,8 +34,19 @@ module Files
3234
# Never use a reply's ts value; use its parent instead.
3335
# Also make sure to provide only one channel when using 'thread_ts'.
3436
def files_upload_v2(params = {})
35-
%i[filename content].each do |param|
36-
raise ArgumentError, "Required argument :#{param} missing" if params[param].nil?
37+
# Normalize input to always work with an array of files
38+
files_to_upload = if params[:files] && params[:files].is_a?(Array)
39+
params[:files]
40+
else
41+
# Convert single file parameters to array format
42+
[params.slice(:filename, :content, :title, :alt_txt, :snippet_type)]
43+
end
44+
45+
# Validate all files have required parameters
46+
files_to_upload.each do |file|
47+
%i[filename content].each do |param|
48+
raise ArgumentError, "Required argument :#{param} missing in file" if file[param].nil?
49+
end
3750
end
3851

3952
channel_params = %i[channel channels channel_id].map { |param| params[param] }.compact
@@ -53,31 +66,34 @@ def files_upload_v2(params = {})
5366
complete_upload_request_params[:channel_id] = params[:channel_id]
5467
end
5568

56-
content = params[:content]
57-
title = params[:title] || params[:filename]
69+
# Upload all files using the same logic
70+
uploaded_files = files_to_upload.map do |file|
71+
content = file[:content]
72+
title = file[:title] || file[:filename]
73+
74+
upload_url_request_params = file.slice(:filename, :alt_txt, :snippet_type)
75+
upload_url_request_params[:length] = content.bytesize
5876

59-
upload_url_request_params = params.slice(:filename, :alt_txt, :snippet_type)
60-
upload_url_request_params[:length] = content.bytesize
77+
# Get the upload url.
78+
get_upload_url_response = files_getUploadURLExternal(upload_url_request_params)
79+
upload_url = get_upload_url_response[:upload_url]
80+
file_id = get_upload_url_response[:file_id]
6181

62-
# Get the upload url.
63-
get_upload_url_response = files_getUploadURLExternal(upload_url_request_params)
64-
upload_url = get_upload_url_response[:upload_url]
65-
file_id = get_upload_url_response[:file_id]
82+
# Upload the file.
83+
::Faraday::Connection.new(upload_url, options) do |connection|
84+
connection.request :multipart
85+
connection.request :url_encoded
86+
connection.use ::Slack::Web::Faraday::Response::WrapError
87+
connection.response :logger, logger if logger
88+
connection.adapter adapter
89+
end.post do |request|
90+
request.body = content
91+
end
6692

67-
# Upload the file.
68-
::Faraday::Connection.new(upload_url, options) do |connection|
69-
connection.request :multipart
70-
connection.request :url_encoded
71-
connection.use ::Slack::Web::Faraday::Response::WrapError
72-
connection.response :logger, logger if logger
73-
connection.adapter adapter
74-
end.post do |request|
75-
request.body = content
93+
{ id: file_id, title: title }
7694
end
7795

78-
# Complete the upload.
79-
complete_upload_request_params[:files] = [{ id: file_id, title: title }].to_json
80-
96+
complete_upload_request_params[:files] = uploaded_files.to_json
8197
files_completeUploadExternal(complete_upload_request_params)
8298
end
8399
end

spec/fixtures/slack/web/files_upload_v2_multiple_files.yml

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/slack/web/api/endpoints/custom/files_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,45 @@
141141
).files.size).to eq 1
142142
end
143143
end
144+
145+
context 'with multiple files', vcr: { cassette_name: 'web/files_upload_v2_multiple_files' } do
146+
it 'completes the upload with multiple files' do
147+
expect(client.files_upload_v2(
148+
files: [
149+
{ filename: 'test1.txt', content: 'First file contents', title: 'First File' },
150+
{ filename: 'test2.txt', content: 'Second file contents', title: 'Second File' }
151+
],
152+
channels: 'C08AZ76CA4V',
153+
initial_comment: 'Multiple files upload test'
154+
).files.size).to eq 2
155+
end
156+
end
157+
158+
context 'with multiple files missing filename' do
159+
it 'raises an argument error' do
160+
expect do
161+
client.files_upload_v2(
162+
files: [
163+
{ content: 'First file contents', title: 'First File' },
164+
{ filename: 'test2.txt', content: 'Second file contents' }
165+
],
166+
channels: 'C08AZ76CA4V'
167+
)
168+
end.to raise_error ArgumentError, /Required argument :filename missing in file/
169+
end
170+
end
171+
172+
context 'with multiple files missing content' do
173+
it 'raises an argument error' do
174+
expect do
175+
client.files_upload_v2(
176+
files: [
177+
{ filename: 'test1.txt', title: 'First File' },
178+
{ filename: 'test2.txt', content: 'Second file contents' }
179+
],
180+
channels: 'C08AZ76CA4V'
181+
)
182+
end.to raise_error ArgumentError, /Required argument :content missing in file/
183+
end
184+
end
144185
end

0 commit comments

Comments
 (0)