-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Endpoint for uploading FA.exe in fafbeta and fafdevelop (PR #328)
fixes #327
- Loading branch information
1 parent
f127590
commit 545a0b8
Showing
16 changed files
with
577 additions
and
18 deletions.
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
src/inttest/java/com/faforever/api/deployment/ExeUploadControllerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package com.faforever.api.deployment; | ||
|
||
import com.faforever.api.AbstractIntegrationTest; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.springframework.mock.web.MockMultipartFile; | ||
import org.springframework.test.context.jdbc.Sql; | ||
|
||
import java.nio.file.Files; | ||
import java.nio.file.Paths; | ||
|
||
import static org.junit.Assert.assertTrue; | ||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
||
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepFeaturedMods.sql") | ||
@Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, scripts = "classpath:sql/cleanFeaturedMods.sql") | ||
public class ExeUploadControllerTest extends AbstractIntegrationTest { | ||
private MockMultipartFile file; | ||
private static final String SUPER_SECRET = "banana"; | ||
|
||
@Before | ||
public void setUp() { | ||
super.setUp(); | ||
file = new MockMultipartFile("file", "ForgedAlliance.exe", "application/octet-stream", new byte[]{ 1, 2 ,3, 4 }); | ||
} | ||
|
||
@Test | ||
public void testSuccessUploadBeta() throws Exception { | ||
this.mockMvc.perform(fileUpload("/exe/upload") | ||
.file(file) | ||
.param("modName", "fafbeta") | ||
.param("apiKey", SUPER_SECRET) | ||
).andExpect(status().isOk()); | ||
assertTrue(Files.exists(Paths.get("build/exe/beta/ForgedAlliance.3706.exe"))); | ||
} | ||
|
||
@Test | ||
public void testSuccessUploadDevelop() throws Exception { | ||
this.mockMvc.perform(fileUpload("/exe/upload") | ||
.file(file) | ||
.param("modName", "fafdevelop") | ||
.param("apiKey", SUPER_SECRET) | ||
).andExpect(status().isOk()); | ||
assertTrue(Files.exists(Paths.get("build/exe/develop/ForgedAlliance.3707.exe"))); | ||
} | ||
|
||
@Test | ||
public void testBadRequestUploadNoModName() throws Exception { | ||
this.mockMvc.perform(fileUpload("/exe/upload") | ||
.file(file) | ||
.param("apiKey", SUPER_SECRET) | ||
).andExpect(status().is4xxClientError()); | ||
} | ||
|
||
@Test | ||
public void testBadRequestUploadNoFile() throws Exception { | ||
this.mockMvc.perform(fileUpload("/exe/upload") | ||
.param("modName", "fafdevelop") | ||
.param("apiKey", SUPER_SECRET) | ||
).andExpect(status().is4xxClientError()); | ||
} | ||
|
||
@Test | ||
public void testBadRequestUploadFileWithWrongExeExtension() throws Exception { | ||
MockMultipartFile file = new MockMultipartFile("file", "ForgedAlliance.zip", "application/octet-stream", new byte[]{ 1, 2 ,3, 4 }); | ||
this.mockMvc.perform(fileUpload("/exe/upload") | ||
.file(file) | ||
.param("modName", "fafbeta") | ||
.param("apiKey", SUPER_SECRET) | ||
).andExpect(status().is4xxClientError()); | ||
} | ||
|
||
@Test | ||
public void testBadRequestUploadWithoutApiKey() throws Exception { | ||
this.mockMvc.perform(fileUpload("/exe/upload") | ||
.file(file) | ||
.param("modName", "fafbeta") | ||
).andExpect(status().is4xxClientError()); | ||
} | ||
|
||
@Test | ||
public void testBadRequestUploadWithWrongApiKey() throws Exception { | ||
this.mockMvc.perform(fileUpload("/exe/upload") | ||
.file(file) | ||
.param("modName", "fafbeta") | ||
.param("apiKey", "not a banana") | ||
).andExpect(status().is4xxClientError()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
DELETE FROM updates_fafdevelop_files; | ||
DELETE FROM updates_fafbeta_files; | ||
DELETE FROM updates_fafdevelop; | ||
DELETE FROM updates_fafbeta; | ||
DELETE FROM game_featuredMods; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
DELETE FROM updates_fafdevelop_files; | ||
DELETE FROM updates_fafdevelop; | ||
DELETE FROM updates_fafbeta_files; | ||
DELETE FROM updates_fafbeta; | ||
DELETE FROM reported_user; | ||
DELETE FROM moderation_report; | ||
DELETE FROM game_stats; | ||
DELETE FROM game_featuredMods; | ||
|
||
INSERT INTO `game_featuredMods` (`id`, `gamemod`, `description`, `name`, `publish`, `order`, `git_url`, `git_branch`, `file_extension`, `allow_override`, `deployment_webhook`) VALUES | ||
(1, 'faf', 'Forged Alliance Forever', 'FAF', 1, 0, 'https://github.com/FAForever/fa.git', 'deploy/faf', 'nx2', 0, NULL), | ||
(27, 'fafbeta', 'Beta version of the next FAF patch', 'FAF Beta', 1, 2, 'https://github.com/FAForever/fa.git', 'deploy/fafbeta', 'nx4', 1, NULL), | ||
(28, 'fafdevelop', 'Updated frequently for testing the upcoming game Patch', 'FAF Develop', 1, 11, 'https://github.com/FAForever/fa.git', 'deploy/fafdevelop', 'nx5', 1, NULL); | ||
|
||
INSERT INTO `updates_fafbeta` (`id`, `filename`, `path`) VALUES | ||
(1, 'ForgedAlliance.exe', 'bin'); | ||
|
||
INSERT INTO `updates_fafbeta_files` (`id`, `fileId`, `version`, `name`, `md5`, `obselete`) VALUES | ||
(1703, 1, 3706, 'ForgedAlliance.3706.exe', 'c20b922a785cf5876c39b7696a16f162', 0); | ||
|
||
INSERT INTO `updates_fafdevelop` (`id`, `filename`, `path`) VALUES | ||
(1, 'ForgedAlliance.exe', 'bin'); | ||
|
||
INSERT INTO `updates_fafdevelop_files` (`id`, `fileId`, `version`, `name`, `md5`, `obselete`) VALUES | ||
(4327, 1, 3707, 'ForgedAlliance.3707.exe', '79f0ea70625ab464d369721183e9fd29', 0); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
src/main/java/com/faforever/api/deployment/ExeUploaderController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package com.faforever.api.deployment; | ||
|
||
import com.faforever.api.config.FafApiProperties; | ||
import com.faforever.api.error.ApiException; | ||
import com.faforever.api.error.Error; | ||
import com.faforever.api.error.ErrorCode; | ||
import com.google.common.io.Files; | ||
import io.swagger.annotations.ApiOperation; | ||
import io.swagger.annotations.ApiResponse; | ||
import io.swagger.annotations.ApiResponses; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestMethod; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; | ||
|
||
@RestController | ||
@RequestMapping(path = "/exe") | ||
@Slf4j | ||
public class ExeUploaderController { | ||
private final FafApiProperties apiProperties; | ||
private final ExeUploaderService exeUploaderService; | ||
|
||
public ExeUploaderController( | ||
FafApiProperties apiProperties, | ||
ExeUploaderService exeUploaderService | ||
) { | ||
this.apiProperties = apiProperties; | ||
this.exeUploaderService = exeUploaderService; | ||
} | ||
|
||
@ApiOperation("Upload an exe file and override the existing one for the current version") | ||
@ApiResponses(value = { | ||
@ApiResponse(code = 200, message = "Success"), | ||
@ApiResponse(code = 401, message = "Unauthorized"), | ||
@ApiResponse(code = 500, message = "Failure")}) | ||
@RequestMapping(path = "/upload", method = RequestMethod.POST, produces = APPLICATION_JSON_UTF8_VALUE) | ||
public void upload(@RequestParam("file") MultipartFile file, | ||
@RequestParam("modName") String modName, | ||
@RequestParam("apiKey") String apiKey | ||
) throws Exception { | ||
if (!apiKey.equals(apiProperties.getDeployment().getTestingExeUploadKey())) { | ||
throw new ApiException(new Error(ErrorCode.API_KEY_INVALID)); | ||
} | ||
String extension = Files.getFileExtension(file.getOriginalFilename()); | ||
if (!apiProperties.getDeployment().getAllowedExeExtension().equals(extension)) { | ||
throw new ApiException( | ||
new Error(ErrorCode.UPLOAD_INVALID_FILE_EXTENSIONS, apiProperties.getDeployment().getAllowedExeExtension()) | ||
); | ||
} | ||
|
||
log.info("Uploading exe file '{}' to '{}' directory", file.getOriginalFilename(), modName); | ||
|
||
exeUploaderService.processUpload(file.getInputStream(), modName); | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
src/main/java/com/faforever/api/deployment/ExeUploaderService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package com.faforever.api.deployment; | ||
|
||
import com.faforever.api.config.FafApiProperties; | ||
import com.faforever.api.content.ContentService; | ||
import com.faforever.api.error.ApiException; | ||
import com.faforever.api.error.Error; | ||
import com.faforever.api.error.ErrorCode; | ||
import com.faforever.api.featuredmods.FeaturedModFile; | ||
import com.faforever.api.featuredmods.FeaturedModService; | ||
import com.faforever.api.utils.FilePermissionUtil; | ||
import lombok.SneakyThrows; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
import org.springframework.util.Assert; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.nio.file.StandardCopyOption; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
import static com.google.common.hash.Hashing.md5; | ||
import static com.google.common.io.Files.asByteSource; | ||
import static java.nio.file.Files.createDirectories; | ||
|
||
@Service | ||
@Slf4j | ||
public class ExeUploaderService { | ||
private final ContentService contentService; | ||
private final FafApiProperties apiProperties; | ||
private final FeaturedModService featuredModService; | ||
|
||
public ExeUploaderService( | ||
ContentService contentService, | ||
FafApiProperties apiProperties, | ||
FeaturedModService featuredModService | ||
) { | ||
this.contentService = contentService; | ||
this.apiProperties = apiProperties; | ||
this.featuredModService = featuredModService; | ||
} | ||
|
||
@Transactional | ||
@SneakyThrows | ||
public void processUpload(InputStream exeDataInputStream, String modName) { | ||
checkAllowedBranchName(modName); | ||
FeaturedModFile featuredModFile = featuredModService.getFile(modName, null, "ForgedAlliance.exe"); | ||
featuredModFile.setName(String.format("ForgedAlliance.%d.exe", featuredModFile.getVersion())); | ||
Path uploadedFile = this.upload( | ||
exeDataInputStream, | ||
featuredModFile.getName(), | ||
modName | ||
); | ||
featuredModFile.setMd5(asByteSource(uploadedFile.toFile()).hash(md5()).toString()); | ||
exeDataInputStream.close(); | ||
List<FeaturedModFile> featuredModFiles = Collections.singletonList(featuredModFile); | ||
featuredModService.save(modName, (short) featuredModFile.getVersion(), featuredModFiles); | ||
} | ||
|
||
private Path upload(InputStream exeDataInputStream, String fileName, String modName) throws IOException { | ||
Assert.isTrue(exeDataInputStream.available() > 0, "data of 'ForgedAlliance.exe' must not be empty"); | ||
|
||
Path tempDir = contentService.createTempDir(); | ||
Path temporaryFile = tempDir.resolve(fileName); | ||
Files.copy(exeDataInputStream, temporaryFile); | ||
|
||
Path copyTo = getCopyToPath(modName, fileName); | ||
createDirectories(copyTo.getParent(), FilePermissionUtil.directoryPermissionFileAttributes()); | ||
return Files.copy( | ||
temporaryFile, | ||
copyTo, | ||
StandardCopyOption.REPLACE_EXISTING | ||
); | ||
} | ||
|
||
private void checkAllowedBranchName(String modName) throws ApiException { | ||
if (!"fafbeta".equals(modName) && !"fafdevelop".equals(modName)) { | ||
throw new ApiException(new Error(ErrorCode.INVALID_FEATURED_MOD, modName)); | ||
} | ||
} | ||
|
||
private Path getCopyToPath(String modName, String fileName) { | ||
String copyTo = null; | ||
switch (modName) { | ||
case "fafbeta": | ||
copyTo = apiProperties.getDeployment().getForgedAllianceBetaExePath(); | ||
break; | ||
case "fafdevelop": | ||
copyTo = apiProperties.getDeployment().getForgedAllianceDevelopExePath(); | ||
break; | ||
default: | ||
throw new ApiException(new Error(ErrorCode.INVALID_FEATURED_MOD, modName)); | ||
} | ||
|
||
return Paths.get(copyTo, fileName); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.