diff --git a/src/main/java/jenkins/plugins/slack/pipeline/SlackUploadFileRunner.java b/src/main/java/jenkins/plugins/slack/pipeline/SlackUploadFileRunner.java index 7c886f00..0c0b55bc 100644 --- a/src/main/java/jenkins/plugins/slack/pipeline/SlackUploadFileRunner.java +++ b/src/main/java/jenkins/plugins/slack/pipeline/SlackUploadFileRunner.java @@ -15,25 +15,29 @@ import jenkins.plugins.slack.HttpClient; import jenkins.security.MasterToSlaveCallable; import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; +import org.json.JSONArray; import org.json.JSONObject; public class SlackUploadFileRunner extends MasterToSlaveCallable implements Serializable { private static final long serialVersionUID = 1L; - private static final String API_URL = "https://slack.com/api/files.upload"; + private static final String GET_UPLOAD_URL_API = "https://slack.com/api/files.getUploadURLExternal"; private static final Logger logger = Logger.getLogger(SlackUploadFileRunner.class.getName()); private static final String UPLOAD_FAILED_TEMPLATE = "Slack upload may have failed. Response: "; private final FilePath filePath; - private String fileToUploadPath; + private final String fileToUploadPath; private final String channels; @@ -55,9 +59,9 @@ public SlackUploadFileRunner(TaskListener listener, ProxyConfiguration proxy, Sl @Override public Boolean call() throws Throwable { - logger.info(filePath + ""); - logger.info(fileToUploadPath); - listener.getLogger().println(String.format("Using dirname=%s and includeMask=%s", filePath.getRemote(), fileToUploadPath)); + logger.fine(filePath + ""); + logger.fine(fileToUploadPath); + listener.getLogger().printf("Using dirname=%s and includeMask=%s%n", filePath.getRemote(), fileToUploadPath); final List files = new ArrayList<>(); new DirScanner.Glob(fileToUploadPath, null).scan(new File(filePath.getRemote()), new FileVisitor() { @@ -79,59 +83,166 @@ public void visit(File file, String relativePath) { } private boolean doIt(List files) { - CloseableHttpClient client = HttpClient.getCloseableHttpClient(proxy); - String threadTs = null; - String theChannels = channels; - - //thread_ts is passed once with roomId: Ex: roomId:threadTs - String[] splitThread = channels.split(":", 2); - if (splitThread.length == 2) { - theChannels = splitThread[0]; - threadTs = splitThread[1]; - } - for (File file:files) { - MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create() + String threadTs = null; + String theChannels = channels; + + //thread_ts is passed once with roomId: Ex: roomId:threadTs + String[] splitThread = channels.split(":", 2); + if (splitThread.length == 2) { + theChannels = splitThread[0]; + threadTs = splitThread[1]; + } + + List fileIds = new ArrayList<>(); + try (CloseableHttpClient client = HttpClient.getCloseableHttpClient(proxy)) { + for (File file : files) { + MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create() .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) - .addTextBody("channels", theChannels, ContentType.DEFAULT_TEXT) .addBinaryBody("file", file, ContentType.DEFAULT_BINARY, file.getName()); - - if (initialComment != null) { - multipartEntityBuilder = multipartEntityBuilder - .addTextBody("initial_comment", initialComment, ContentType.DEFAULT_TEXT); + JSONObject getUploadUrlResult = getUploadUrlExternal(file, client); + if (getUploadUrlResult == null) { + return false; } - if (threadTs != null) { - multipartEntityBuilder = multipartEntityBuilder - .addTextBody("thread_ts", threadTs, ContentType.DEFAULT_TEXT); - } + String uploadUrl = getUploadUrlResult.getString("upload_url"); - HttpUriRequest request = RequestBuilder - .post(API_URL) - .setEntity(multipartEntityBuilder.build()) - .addHeader("Authorization", "Bearer " + token) - .build(); - ResponseHandler responseHandler = response -> { - int status = response.getStatusLine().getStatusCode(); - if (status >= 200 && status < 300) { - HttpEntity entity = response.getEntity(); - return entity != null ? new org.json.JSONObject(EntityUtils.toString(entity)) : null; - } else { - logger.log(Level.WARNING, UPLOAD_FAILED_TEMPLATE + status); - return null; - } - }; - try { - org.json.JSONObject responseBody = client.execute(request, responseHandler); - if (responseBody != null && !responseBody.getBoolean("ok")) { - listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + responseBody.toString()); - return false; - } - } catch (IOException e) { - String msg = "Exception uploading files '" + file + "' to Slack "; - logger.log(Level.WARNING, msg, e); - listener.getLogger().println(msg + e.getMessage()); + if (!uploadFile(uploadUrl, multipartEntityBuilder, client)) { + listener.getLogger().println("Failed to upload file to Slack"); + return false; } + String fileId = getUploadUrlResult.getString("file_id"); + fileIds.add(fileId); + } + String channelId = convertChannelNameToId(theChannels, client); + if (!completeUploadExternal(channelId, threadTs, fileIds, client)) { + listener.getLogger().println("Failed to complete uploading file to Slack"); + return false; } + + } catch (IOException e) { + String msg = "Exception uploading to Slack "; + logger.log(Level.WARNING, msg, e); + listener.getLogger().println(msg + e.getMessage()); + } + return true; + } + + private boolean completeUploadExternal(String channelId, String threadTs, List fileIds, CloseableHttpClient client) throws IOException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("channel_id", channelId); + if (initialComment != null) { + jsonObject.put("initial_comment", initialComment); + } + if (threadTs != null) { + jsonObject.put("thread_ts", threadTs); + } + + jsonObject.put("files", convertListToJsonArray(fileIds)); + HttpUriRequest completeRequest = RequestBuilder + .post("https://slack.com/api/files.completeUploadExternal") + .setEntity(new StringEntity(jsonObject.toString(), ContentType.APPLICATION_JSON)) + .addHeader("Authorization", "Bearer " + token) + .build(); + + JSONObject completeRequestResponse = client.execute(completeRequest, getStandardResponseHandler()); + + if (completeRequestResponse != null && !completeRequestResponse.getBoolean("ok")) { + listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + completeRequestResponse); + return false; + } + return true; } + + private static JSONArray convertListToJsonArray(List fileIds) { + JSONArray jsonArray = new JSONArray(); + fileIds.stream() + .map(fileId -> new JSONObject().put("id", fileId)) + .forEach(jsonArray::put); + return jsonArray; + } + + private static ResponseHandler getStandardResponseHandler() { + return response -> { + int status = response.getStatusLine().getStatusCode(); + if (status >= 200 && status < 300) { + HttpEntity entity = response.getEntity(); + return entity != null ? new JSONObject(EntityUtils.toString(entity)) : null; + } else { + logger.log(Level.WARNING, UPLOAD_FAILED_TEMPLATE + status); + return null; + } + }; + } + + private String convertChannelNameToId(String channels, CloseableHttpClient client) throws IOException { + return convertChannelNameToId(channels, client, null); + } + + private String convertChannelNameToId(String channelName, CloseableHttpClient client, String cursor) throws IOException { + RequestBuilder requestBuilder = RequestBuilder.get("https://slack.com/api/conversations.list") + .addHeader("Authorization", "Bearer " + token) + .addParameter("exclude_archived", "true") + .addParameter("types", "public_channel,private_channel"); + + if (cursor != null) { + requestBuilder.addParameter("cursor", cursor); + } + ResponseHandler standardResponseHandler = getStandardResponseHandler(); + JSONObject result = client.execute(requestBuilder.build(), standardResponseHandler); + + if (result == null || !result.getBoolean("ok")) { + return null; + } + + JSONArray channelsArray = result.getJSONArray("channels"); + for (int i = 0; i < channelsArray.length(); i++) { + JSONObject channel = channelsArray.getJSONObject(i); + if (channel.getString("name").equals(channelName)) { + return channel.getString("id"); + } + } + + cursor = result.getJSONObject("response_metadata").getString("next_cursor"); + if (cursor != null && !cursor.isEmpty()) { + return convertChannelNameToId(channelName, client, cursor); + } + + listener.getLogger().println("Couldn't find channel id for channel name " + channelName); + + return null; + } + + private boolean uploadFile(String uploadUrl, MultipartEntityBuilder multipartEntityBuilder, CloseableHttpClient client) throws IOException { + HttpUriRequest request = RequestBuilder + .post(uploadUrl) + .setEntity(multipartEntityBuilder.build()) + .addHeader("Authorization", "Bearer " + token) + .build(); + + try (CloseableHttpResponse responseBody = client.execute(request)) { + if (responseBody.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + EntityUtils.toString(responseBody.getEntity())); + return false; + } + } + return true; + } + + private JSONObject getUploadUrlExternal(File file, CloseableHttpClient client) throws IOException { + HttpUriRequest getUploadApiRequest = RequestBuilder.get(GET_UPLOAD_URL_API) + .addParameter("filename", file.getName()) + .addParameter("length", String.valueOf(file.length())) + .addHeader("Authorization", "Bearer " + token) + .build(); + JSONObject getUploadRequestResponse = client.execute(getUploadApiRequest, getStandardResponseHandler()); + if (getUploadRequestResponse != null && !getUploadRequestResponse.getBoolean("ok")) { + listener.getLogger().println(UPLOAD_FAILED_TEMPLATE + getUploadRequestResponse); + return null; + } else if (getUploadRequestResponse == null) { + listener.getLogger().println(UPLOAD_FAILED_TEMPLATE); + return null; + } + return getUploadRequestResponse; + } } diff --git a/src/main/resources/jenkins/plugins/slack/pipeline/SlackUploadFileStep/help-channel.html b/src/main/resources/jenkins/plugins/slack/pipeline/SlackUploadFileStep/help-channel.html index 765fc6e0..4c57d7f5 100644 --- a/src/main/resources/jenkins/plugins/slack/pipeline/SlackUploadFileStep/help-channel.html +++ b/src/main/resources/jenkins/plugins/slack/pipeline/SlackUploadFileStep/help-channel.html @@ -1,6 +1,5 @@
Allows overriding the Slack Plugin channel specified in the global configuration. - Multiple channels may be provided as a comma separated string.
slackUploadFile channel: "#channel-name", filePath: "file.txt"